diff --git a/resources/sql/autopatches/20150922.drydock.commands.1.sql b/resources/sql/autopatches/20150922.drydock.commands.1.sql new file mode 100644 index 0000000000..173fe861ac --- /dev/null +++ b/resources/sql/autopatches/20150922.drydock.commands.1.sql @@ -0,0 +1,10 @@ +CREATE TABLE {$NAMESPACE}_drydock.drydock_command ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + authorPHID VARBINARY(64) NOT NULL, + targetPHID VARBINARY(64) NOT NULL, + command VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}, + isConsumed BOOL NOT NULL, + dateCreated INT UNSIGNED NOT NULL, + dateModified INT UNSIGNED NOT NULL, + KEY `key_target` (targetPHID, isConsumed) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 747e085023..229f074c58 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -817,7 +817,9 @@ phutil_register_library_map(array( 'DrydockBlueprintTransaction' => 'applications/drydock/storage/DrydockBlueprintTransaction.php', 'DrydockBlueprintTransactionQuery' => 'applications/drydock/query/DrydockBlueprintTransactionQuery.php', 'DrydockBlueprintViewController' => 'applications/drydock/controller/DrydockBlueprintViewController.php', + 'DrydockCommand' => 'applications/drydock/storage/DrydockCommand.php', 'DrydockCommandInterface' => 'applications/drydock/interface/command/DrydockCommandInterface.php', + 'DrydockCommandQuery' => 'applications/drydock/query/DrydockCommandQuery.php', 'DrydockConsoleController' => 'applications/drydock/controller/DrydockConsoleController.php', 'DrydockConstants' => 'applications/drydock/constants/DrydockConstants.php', 'DrydockController' => 'applications/drydock/controller/DrydockController.php', @@ -837,6 +839,7 @@ phutil_register_library_map(array( 'DrydockLeaseReleaseController' => 'applications/drydock/controller/DrydockLeaseReleaseController.php', 'DrydockLeaseSearchEngine' => 'applications/drydock/query/DrydockLeaseSearchEngine.php', 'DrydockLeaseStatus' => 'applications/drydock/constants/DrydockLeaseStatus.php', + 'DrydockLeaseUpdateWorker' => 'applications/drydock/worker/DrydockLeaseUpdateWorker.php', 'DrydockLeaseViewController' => 'applications/drydock/controller/DrydockLeaseViewController.php', 'DrydockLeaseWorker' => 'applications/drydock/worker/DrydockLeaseWorker.php', 'DrydockLog' => 'applications/drydock/storage/DrydockLog.php', @@ -845,22 +848,25 @@ phutil_register_library_map(array( 'DrydockLogListView' => 'applications/drydock/view/DrydockLogListView.php', 'DrydockLogQuery' => 'applications/drydock/query/DrydockLogQuery.php', 'DrydockLogSearchEngine' => 'applications/drydock/query/DrydockLogSearchEngine.php', - 'DrydockManagementCloseWorkflow' => 'applications/drydock/management/DrydockManagementCloseWorkflow.php', 'DrydockManagementCommandWorkflow' => 'applications/drydock/management/DrydockManagementCommandWorkflow.php', 'DrydockManagementLeaseWorkflow' => 'applications/drydock/management/DrydockManagementLeaseWorkflow.php', - 'DrydockManagementReleaseWorkflow' => 'applications/drydock/management/DrydockManagementReleaseWorkflow.php', + 'DrydockManagementReleaseLeaseWorkflow' => 'applications/drydock/management/DrydockManagementReleaseLeaseWorkflow.php', + 'DrydockManagementReleaseResourceWorkflow' => 'applications/drydock/management/DrydockManagementReleaseResourceWorkflow.php', + 'DrydockManagementUpdateLeaseWorkflow' => 'applications/drydock/management/DrydockManagementUpdateLeaseWorkflow.php', + 'DrydockManagementUpdateResourceWorkflow' => 'applications/drydock/management/DrydockManagementUpdateResourceWorkflow.php', 'DrydockManagementWorkflow' => 'applications/drydock/management/DrydockManagementWorkflow.php', 'DrydockQuery' => 'applications/drydock/query/DrydockQuery.php', 'DrydockResource' => 'applications/drydock/storage/DrydockResource.php', - 'DrydockResourceCloseController' => 'applications/drydock/controller/DrydockResourceCloseController.php', 'DrydockResourceController' => 'applications/drydock/controller/DrydockResourceController.php', 'DrydockResourceDatasource' => 'applications/drydock/typeahead/DrydockResourceDatasource.php', 'DrydockResourceListController' => 'applications/drydock/controller/DrydockResourceListController.php', 'DrydockResourceListView' => 'applications/drydock/view/DrydockResourceListView.php', 'DrydockResourcePHIDType' => 'applications/drydock/phid/DrydockResourcePHIDType.php', 'DrydockResourceQuery' => 'applications/drydock/query/DrydockResourceQuery.php', + 'DrydockResourceReleaseController' => 'applications/drydock/controller/DrydockResourceReleaseController.php', 'DrydockResourceSearchEngine' => 'applications/drydock/query/DrydockResourceSearchEngine.php', 'DrydockResourceStatus' => 'applications/drydock/constants/DrydockResourceStatus.php', + 'DrydockResourceUpdateWorker' => 'applications/drydock/worker/DrydockResourceUpdateWorker.php', 'DrydockResourceViewController' => 'applications/drydock/controller/DrydockResourceViewController.php', 'DrydockResourceWorker' => 'applications/drydock/worker/DrydockResourceWorker.php', 'DrydockSFTPFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php', @@ -4535,7 +4541,12 @@ phutil_register_library_map(array( 'DrydockBlueprintTransaction' => 'PhabricatorApplicationTransaction', 'DrydockBlueprintTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'DrydockBlueprintViewController' => 'DrydockBlueprintController', + 'DrydockCommand' => array( + 'DrydockDAO', + 'PhabricatorPolicyInterface', + ), 'DrydockCommandInterface' => 'DrydockInterface', + 'DrydockCommandQuery' => 'DrydockQuery', 'DrydockConsoleController' => 'DrydockController', 'DrydockConstants' => 'Phobject', 'DrydockController' => 'PhabricatorController', @@ -4558,6 +4569,7 @@ phutil_register_library_map(array( 'DrydockLeaseReleaseController' => 'DrydockLeaseController', 'DrydockLeaseSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockLeaseStatus' => 'DrydockConstants', + 'DrydockLeaseUpdateWorker' => 'DrydockWorker', 'DrydockLeaseViewController' => 'DrydockLeaseController', 'DrydockLeaseWorker' => 'DrydockWorker', 'DrydockLog' => array( @@ -4569,25 +4581,28 @@ phutil_register_library_map(array( 'DrydockLogListView' => 'AphrontView', 'DrydockLogQuery' => 'DrydockQuery', 'DrydockLogSearchEngine' => 'PhabricatorApplicationSearchEngine', - 'DrydockManagementCloseWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementCommandWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementLeaseWorkflow' => 'DrydockManagementWorkflow', - 'DrydockManagementReleaseWorkflow' => 'DrydockManagementWorkflow', + 'DrydockManagementReleaseLeaseWorkflow' => 'DrydockManagementWorkflow', + 'DrydockManagementReleaseResourceWorkflow' => 'DrydockManagementWorkflow', + 'DrydockManagementUpdateLeaseWorkflow' => 'DrydockManagementWorkflow', + 'DrydockManagementUpdateResourceWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow', 'DrydockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DrydockResource' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', ), - 'DrydockResourceCloseController' => 'DrydockResourceController', 'DrydockResourceController' => 'DrydockController', 'DrydockResourceDatasource' => 'PhabricatorTypeaheadDatasource', 'DrydockResourceListController' => 'DrydockResourceController', 'DrydockResourceListView' => 'AphrontView', 'DrydockResourcePHIDType' => 'PhabricatorPHIDType', 'DrydockResourceQuery' => 'DrydockQuery', + 'DrydockResourceReleaseController' => 'DrydockResourceController', 'DrydockResourceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockResourceStatus' => 'DrydockConstants', + 'DrydockResourceUpdateWorker' => 'DrydockWorker', 'DrydockResourceViewController' => 'DrydockResourceController', 'DrydockResourceWorker' => 'DrydockWorker', 'DrydockSFTPFilesystemInterface' => 'DrydockFilesystemInterface', diff --git a/src/applications/drydock/application/PhabricatorDrydockApplication.php b/src/applications/drydock/application/PhabricatorDrydockApplication.php index 7919f1c9cf..d3a543a4f0 100644 --- a/src/applications/drydock/application/PhabricatorDrydockApplication.php +++ b/src/applications/drydock/application/PhabricatorDrydockApplication.php @@ -55,8 +55,10 @@ final class PhabricatorDrydockApplication extends PhabricatorApplication { ), 'resource/' => array( '(?:query/(?P[^/]+)/)?' => 'DrydockResourceListController', - '(?P[1-9]\d*)/' => 'DrydockResourceViewController', - '(?P[1-9]\d*)/close/' => 'DrydockResourceCloseController', + '(?P[1-9]\d*)/' => array( + '' => 'DrydockResourceViewController', + 'release/' => 'DrydockResourceReleaseController', + ), ), 'lease/' => array( '(?:query/(?P[^/]+)/)?' => 'DrydockLeaseListController', diff --git a/src/applications/drydock/controller/DrydockController.php b/src/applications/drydock/controller/DrydockController.php index 3e3d83cc1d..e0130bdf56 100644 --- a/src/applications/drydock/controller/DrydockController.php +++ b/src/applications/drydock/controller/DrydockController.php @@ -36,4 +36,53 @@ abstract class DrydockController extends PhabricatorController { ->addRawContent($table); } + protected function buildCommandsTab($target_phid) { + $viewer = $this->getViewer(); + + $commands = id(new DrydockCommandQuery()) + ->setViewer($viewer) + ->withTargetPHIDs(array($target_phid)) + ->execute(); + + $consumed_yes = id(new PHUIIconView()) + ->setIconFont('fa-check green'); + $consumed_no = id(new PHUIIconView()) + ->setIconFont('fa-clock-o grey'); + + $rows = array(); + foreach ($commands as $command) { + $rows[] = array( + $command->getID(), + $viewer->renderHandle($command->getAuthorPHID()), + $command->getCommand(), + ($command->getIsConsumed() + ? $consumed_yes + : $consumed_no), + phabricator_datetime($command->getDateCreated(), $viewer), + ); + } + + $table = id(new AphrontTableView($rows)) + ->setNoDataString(pht('No commands issued.')) + ->setHeaders( + array( + pht('ID'), + pht('From'), + pht('Command'), + null, + pht('Date'), + )) + ->setColumnClasses( + array( + null, + null, + 'wide', + null, + null, + )); + + return id(new PHUIPropertyListView()) + ->addRawContent($table); + } + } diff --git a/src/applications/drydock/controller/DrydockLeaseReleaseController.php b/src/applications/drydock/controller/DrydockLeaseReleaseController.php index 4bac0330ba..52be31c97a 100644 --- a/src/applications/drydock/controller/DrydockLeaseReleaseController.php +++ b/src/applications/drydock/controller/DrydockLeaseReleaseController.php @@ -9,6 +9,11 @@ final class DrydockLeaseReleaseController extends DrydockLeaseController { $lease = id(new DrydockLeaseQuery()) ->setViewer($viewer) ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) ->executeOne(); if (!$lease) { return new Aphront404Response(); @@ -17,43 +22,35 @@ final class DrydockLeaseReleaseController extends DrydockLeaseController { $lease_uri = '/lease/'.$lease->getID().'/'; $lease_uri = $this->getApplicationURI($lease_uri); - if ($lease->getStatus() != DrydockLeaseStatus::STATUS_ACTIVE) { - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) - ->setTitle(pht('Lease Not Active')) - ->appendChild( - phutil_tag( - 'p', - array(), - pht('You can only release "active" leases.'))) + if (!$lease->canRelease()) { + return $this->newDialog() + ->setTitle(pht('Lease Not Releasable')) + ->appendParagraph( + pht( + 'Leases can not be released after they are destroyed.')) ->addCancelButton($lease_uri); - - return id(new AphrontDialogResponse())->setDialog($dialog); } - if (!$request->isDialogFormPost()) { - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) - ->setTitle(pht('Really release lease?')) - ->appendChild( - phutil_tag( - 'p', - array(), - pht( - 'Releasing a lease may cause trouble for the lease holder and '. - 'trigger cleanup of the underlying resource. It can not be '. - 'undone. Continue?'))) - ->addSubmitButton(pht('Release Lease')) - ->addCancelButton($lease_uri); + if ($request->isFormPost()) { + $command = DrydockCommand::initializeNewCommand($viewer) + ->setTargetPHID($lease->getPHID()) + ->setCommand(DrydockCommand::COMMAND_RELEASE) + ->save(); - return id(new AphrontDialogResponse())->setDialog($dialog); + $lease->scheduleUpdate(); + + return id(new AphrontRedirectResponse())->setURI($lease_uri); } - $resource = $lease->getResource(); - $blueprint = $resource->getBlueprint(); - $blueprint->releaseLease($resource, $lease); - - return id(new AphrontReloadResponse())->setURI($lease_uri); + return $this->newDialog() + ->setTitle(pht('Release Lease?')) + ->appendParagraph( + pht( + 'Forcefully releasing a lease may interfere with the operation '. + 'of the lease holder and trigger destruction of the underlying '. + 'resource. It can not be undone.')) + ->addSubmitButton(pht('Release Lease')) + ->addCancelButton($lease_uri); } } diff --git a/src/applications/drydock/controller/DrydockLeaseViewController.php b/src/applications/drydock/controller/DrydockLeaseViewController.php index aed1f6416f..eaa0683c87 100644 --- a/src/applications/drydock/controller/DrydockLeaseViewController.php +++ b/src/applications/drydock/controller/DrydockLeaseViewController.php @@ -43,11 +43,13 @@ final class DrydockLeaseViewController extends DrydockLeaseController { $crumbs->addTextCrumb($title, $lease_uri); $locks = $this->buildLocksTab($lease->getPHID()); + $commands = $this->buildCommandsTab($lease->getPHID()); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties, pht('Properties')) - ->addPropertyList($locks, pht('Slot Locks')); + ->addPropertyList($locks, pht('Slot Locks')) + ->addPropertyList($commands, pht('Commands')); $log_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Lease Logs')) @@ -66,14 +68,20 @@ final class DrydockLeaseViewController extends DrydockLeaseController { } private function buildActionListView(DrydockLease $lease) { + $viewer = $this->getViewer(); + $view = id(new PhabricatorActionListView()) - ->setUser($this->getRequest()->getUser()) + ->setUser($viewer) ->setObjectURI($this->getRequest()->getRequestURI()) ->setObject($lease); $id = $lease->getID(); - $can_release = ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACTIVE); + $can_release = $lease->canRelease(); + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $lease, + PhabricatorPolicyCapability::CAN_EDIT); $view->addAction( id(new PhabricatorActionView()) @@ -81,7 +89,7 @@ final class DrydockLeaseViewController extends DrydockLeaseController { ->setIcon('fa-times') ->setHref($this->getApplicationURI("/lease/{$id}/release/")) ->setWorkflow(true) - ->setDisabled(!$can_release)); + ->setDisabled(!$can_release || !$can_edit)); return $view; } diff --git a/src/applications/drydock/controller/DrydockResourceCloseController.php b/src/applications/drydock/controller/DrydockResourceCloseController.php deleted file mode 100644 index 915bf89452..0000000000 --- a/src/applications/drydock/controller/DrydockResourceCloseController.php +++ /dev/null @@ -1,49 +0,0 @@ -getViewer(); - $id = $request->getURIData('id'); - - $resource = id(new DrydockResourceQuery()) - ->setViewer($viewer) - ->withIDs(array($id)) - ->executeOne(); - if (!$resource) { - return new Aphront404Response(); - } - - $resource_uri = '/resource/'.$resource->getID().'/'; - $resource_uri = $this->getApplicationURI($resource_uri); - - if ($resource->getStatus() != DrydockResourceStatus::STATUS_OPEN) { - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) - ->setTitle(pht('Resource Not Open')) - ->appendChild(phutil_tag('p', array(), pht( - 'You can only close "open" resources.'))) - ->addCancelButton($resource_uri); - - return id(new AphrontDialogResponse())->setDialog($dialog); - } - - if ($request->isFormPost()) { - $resource->closeResource(); - return id(new AphrontReloadResponse())->setURI($resource_uri); - } - - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) - ->setTitle(pht('Really close resource?')) - ->appendChild( - pht( - 'Closing a resource releases all leases and destroys the '. - 'resource. It can not be undone. Continue?')) - ->addSubmitButton(pht('Close Resource')) - ->addCancelButton($resource_uri); - - return id(new AphrontDialogResponse())->setDialog($dialog); - } - -} diff --git a/src/applications/drydock/controller/DrydockResourceReleaseController.php b/src/applications/drydock/controller/DrydockResourceReleaseController.php new file mode 100644 index 0000000000..4e508597ef --- /dev/null +++ b/src/applications/drydock/controller/DrydockResourceReleaseController.php @@ -0,0 +1,56 @@ +getViewer(); + $id = $request->getURIData('id'); + + $resource = id(new DrydockResourceQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$resource) { + return new Aphront404Response(); + } + + $resource_uri = '/resource/'.$resource->getID().'/'; + $resource_uri = $this->getApplicationURI($resource_uri); + + if (!$resource->canRelease()) { + return $this->newDialog() + ->setTitle(pht('Resource Not Releasable')) + ->appendParagraph( + pht( + 'Resources can not be released after they are destroyed.')) + ->addCancelButton($resource_uri); + } + + if ($request->isFormPost()) { + $command = DrydockCommand::initializeNewCommand($viewer) + ->setTargetPHID($resource->getPHID()) + ->setCommand(DrydockCommand::COMMAND_RELEASE) + ->save(); + + $resource->scheduleUpdate(); + + return id(new AphrontRedirectResponse())->setURI($resource_uri); + } + + + return $this->newDialog() + ->setTitle(pht('Really release resource?')) + ->appendChild( + pht( + 'Releasing a resource releases all leases and destroys the '. + 'resource. It can not be undone.')) + ->addSubmitButton(pht('Release Resource')) + ->addCancelButton($resource_uri); + } + +} diff --git a/src/applications/drydock/controller/DrydockResourceViewController.php b/src/applications/drydock/controller/DrydockResourceViewController.php index d4a37af33c..40009e34ce 100644 --- a/src/applications/drydock/controller/DrydockResourceViewController.php +++ b/src/applications/drydock/controller/DrydockResourceViewController.php @@ -55,11 +55,13 @@ final class DrydockResourceViewController extends DrydockResourceController { $crumbs->addTextCrumb(pht('Resource %d', $resource->getID())); $locks = $this->buildLocksTab($resource->getPHID()); + $commands = $this->buildCommandsTab($resource->getPHID()); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties, pht('Properties')) - ->addPropertyList($locks, pht('Slot Locks')); + ->addPropertyList($locks, pht('Slot Locks')) + ->addPropertyList($commands, pht('Commands')); $lease_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Leases')) @@ -83,22 +85,29 @@ final class DrydockResourceViewController extends DrydockResourceController { } private function buildActionListView(DrydockResource $resource) { + $viewer = $this->getViewer(); + $view = id(new PhabricatorActionListView()) - ->setUser($this->getRequest()->getUser()) + ->setUser($viewer) ->setObjectURI($this->getRequest()->getRequestURI()) ->setObject($resource); - $can_close = ($resource->getStatus() == DrydockResourceStatus::STATUS_OPEN); - $uri = '/resource/'.$resource->getID().'/close/'; + $can_release = $resource->canRelease(); + $can_edit = PhabricatorPolicyFilter::hasCapability( + $viewer, + $resource, + PhabricatorPolicyCapability::CAN_EDIT); + + $uri = '/resource/'.$resource->getID().'/release/'; $uri = $this->getApplicationURI($uri); $view->addAction( id(new PhabricatorActionView()) ->setHref($uri) - ->setName(pht('Close Resource')) + ->setName(pht('Release Resource')) ->setIcon('fa-times') ->setWorkflow(true) - ->setDisabled(!$can_close)); + ->setDisabled(!$can_release || !$can_edit)); return $view; } diff --git a/src/applications/drydock/management/DrydockManagementCloseWorkflow.php b/src/applications/drydock/management/DrydockManagementCloseWorkflow.php deleted file mode 100644 index f20b0e692b..0000000000 --- a/src/applications/drydock/management/DrydockManagementCloseWorkflow.php +++ /dev/null @@ -1,49 +0,0 @@ -setName('close') - ->setSynopsis(pht('Close a resource.')) - ->setArguments( - array( - array( - 'name' => 'ids', - 'wildcard' => true, - ), - )); - } - - public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); - - $ids = $args->getArg('ids'); - if (!$ids) { - throw new PhutilArgumentUsageException( - pht('Specify one or more resource IDs to close.')); - } - - $viewer = $this->getViewer(); - - $resources = id(new DrydockResourceQuery()) - ->setViewer($viewer) - ->withIDs($ids) - ->execute(); - - foreach ($ids as $id) { - $resource = idx($resources, $id); - if (!$resource) { - $console->writeErr("%s\n", pht('Resource %d does not exist!', $id)); - } else if ($resource->getStatus() != DrydockResourceStatus::STATUS_OPEN) { - $console->writeErr("%s\n", pht("Resource %d is not 'open'!", $id)); - } else { - $resource->closeResource(); - $console->writeErr("%s\n", pht('Closed resource %d.', $id)); - } - } - - } - -} diff --git a/src/applications/drydock/management/DrydockManagementReleaseLeaseWorkflow.php b/src/applications/drydock/management/DrydockManagementReleaseLeaseWorkflow.php new file mode 100644 index 0000000000..20af18ec21 --- /dev/null +++ b/src/applications/drydock/management/DrydockManagementReleaseLeaseWorkflow.php @@ -0,0 +1,70 @@ +setName('release-lease') + ->setSynopsis(pht('Release a lease.')) + ->setArguments( + array( + array( + 'name' => 'id', + 'param' => 'id', + 'repeat' => true, + 'help' => pht('Lease ID to release.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $ids = $args->getArg('id'); + if (!$ids) { + throw new PhutilArgumentUsageException( + pht( + 'Specify one or more lease IDs to release with "%s".', + '--id')); + } + + $viewer = $this->getViewer(); + $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID(); + + $leases = id(new DrydockLeaseQuery()) + ->setViewer($viewer) + ->withIDs($ids) + ->execute(); + + PhabricatorWorker::setRunAllTasksInProcess(true); + foreach ($ids as $id) { + $lease = idx($leases, $id); + if (!$lease) { + echo tsprintf( + "%s\n", + pht('Lease "%s" does not exist.', $id)); + continue; + } + + if (!$lease->canRelease()) { + echo tsprintf( + "%s\n", + pht('Lease "%s" is not releasable.', $id)); + continue; + } + + $command = DrydockCommand::initializeNewCommand($viewer) + ->setTargetPHID($lease->getPHID()) + ->setAuthorPHID($drydock_phid) + ->setCommand(DrydockCommand::COMMAND_RELEASE) + ->save(); + + $lease->scheduleUpdate(); + + echo tsprintf( + "%s\n", + pht('Scheduled release of lease "%s".', $id)); + } + + } + +} diff --git a/src/applications/drydock/management/DrydockManagementReleaseResourceWorkflow.php b/src/applications/drydock/management/DrydockManagementReleaseResourceWorkflow.php new file mode 100644 index 0000000000..01060a5325 --- /dev/null +++ b/src/applications/drydock/management/DrydockManagementReleaseResourceWorkflow.php @@ -0,0 +1,71 @@ +setName('release-resource') + ->setSynopsis(pht('Release a resource.')) + ->setArguments( + array( + array( + 'name' => 'id', + 'param' => 'id', + 'repeat' => true, + 'help' => pht('Resource ID to release.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $ids = $args->getArg('id'); + if (!$ids) { + throw new PhutilArgumentUsageException( + pht( + 'Specify one or more resource IDs to release with "%s".', + '--id')); + } + + $viewer = $this->getViewer(); + $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID(); + + $resources = id(new DrydockResourceQuery()) + ->setViewer($viewer) + ->withIDs($ids) + ->execute(); + + PhabricatorWorker::setRunAllTasksInProcess(true); + foreach ($ids as $id) { + $resource = idx($resources, $id); + + if (!$resource) { + echo tsprintf( + "%s\n", + pht('Resource "%s" does not exist.', $id)); + continue; + } + + if (!$resource->canRelease()) { + echo tsprintf( + "%s\n", + pht('Resource "%s" is not releasable.', $id)); + continue; + } + + $command = DrydockCommand::initializeNewCommand($viewer) + ->setTargetPHID($resource->getPHID()) + ->setAuthorPHID($drydock_phid) + ->setCommand(DrydockCommand::COMMAND_RELEASE) + ->save(); + + $resource->scheduleUpdate(); + + echo tsprintf( + "%s\n", + pht('Scheduled release of resource "%s".', $id)); + } + + } + +} diff --git a/src/applications/drydock/management/DrydockManagementReleaseWorkflow.php b/src/applications/drydock/management/DrydockManagementReleaseWorkflow.php deleted file mode 100644 index 616a5deb7b..0000000000 --- a/src/applications/drydock/management/DrydockManagementReleaseWorkflow.php +++ /dev/null @@ -1,52 +0,0 @@ -setName('release') - ->setSynopsis(pht('Release a lease.')) - ->setArguments( - array( - array( - 'name' => 'ids', - 'wildcard' => true, - ), - )); - } - - public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); - - $ids = $args->getArg('ids'); - if (!$ids) { - throw new PhutilArgumentUsageException( - pht('Specify one or more lease IDs to release.')); - } - - $viewer = $this->getViewer(); - - $leases = id(new DrydockLeaseQuery()) - ->setViewer($viewer) - ->withIDs($ids) - ->execute(); - - foreach ($ids as $id) { - $lease = idx($leases, $id); - if (!$lease) { - $console->writeErr("%s\n", pht('Lease %d does not exist!', $id)); - } else if ($lease->getStatus() != DrydockLeaseStatus::STATUS_ACTIVE) { - $console->writeErr("%s\n", pht("Lease %d is not 'active'!", $id)); - } else { - $resource = $lease->getResource(); - $blueprint = $resource->getBlueprint(); - $blueprint->releaseLease($resource, $lease); - - $console->writeErr("%s\n", pht('Released lease %d.', $id)); - } - } - - } - -} diff --git a/src/applications/drydock/management/DrydockManagementUpdateLeaseWorkflow.php b/src/applications/drydock/management/DrydockManagementUpdateLeaseWorkflow.php new file mode 100644 index 0000000000..aa74bb0748 --- /dev/null +++ b/src/applications/drydock/management/DrydockManagementUpdateLeaseWorkflow.php @@ -0,0 +1,57 @@ +setName('update-lease') + ->setSynopsis(pht('Update a lease.')) + ->setArguments( + array( + array( + 'name' => 'id', + 'param' => 'id', + 'repeat' => true, + 'help' => pht('Lease ID to update.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $viewer = $this->getViewer(); + + $ids = $args->getArg('id'); + if (!$ids) { + throw new PhutilArgumentUsageException( + pht( + 'Specify one or more lease IDs to update with "%s".', + '--id')); + } + + $leases = id(new DrydockLeaseQuery()) + ->setViewer($viewer) + ->withIDs($ids) + ->execute(); + + PhabricatorWorker::setRunAllTasksInProcess(true); + + foreach ($ids as $id) { + $lease = idx($leases, $id); + + if (!$lease) { + echo tsprintf( + "%s\n", + pht('Lease "%s" does not exist.', $id)); + continue; + } + + echo tsprintf( + "%s\n", + pht('Updating lease "%s".', $id)); + + $lease->scheduleUpdate(); + } + } + +} diff --git a/src/applications/drydock/management/DrydockManagementUpdateResourceWorkflow.php b/src/applications/drydock/management/DrydockManagementUpdateResourceWorkflow.php new file mode 100644 index 0000000000..79928fda8d --- /dev/null +++ b/src/applications/drydock/management/DrydockManagementUpdateResourceWorkflow.php @@ -0,0 +1,58 @@ +setName('update-resource') + ->setSynopsis(pht('Update a resource.')) + ->setArguments( + array( + array( + 'name' => 'id', + 'param' => 'id', + 'repeat' => true, + 'help' => pht('Resource ID to update.'), + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $viewer = $this->getViewer(); + + $ids = $args->getArg('id'); + if (!$ids) { + throw new PhutilArgumentUsageException( + pht( + 'Specify one or more resource IDs to update with "%s".', + '--id')); + } + + $resources = id(new DrydockResourceQuery()) + ->setViewer($viewer) + ->withIDs($ids) + ->execute(); + + PhabricatorWorker::setRunAllTasksInProcess(true); + + foreach ($ids as $id) { + $resource = idx($resources, $id); + + if (!$resource) { + echo tsprintf( + "%s\n", + pht('Resource "%s" does not exist.', $id)); + continue; + } + + echo tsprintf( + "%s\n", + pht('Updating resource "%s".', $id)); + + $resource->scheduleUpdate(); + } + + } + +} diff --git a/src/applications/drydock/query/DrydockCommandQuery.php b/src/applications/drydock/query/DrydockCommandQuery.php new file mode 100644 index 0000000000..0d71288a85 --- /dev/null +++ b/src/applications/drydock/query/DrydockCommandQuery.php @@ -0,0 +1,82 @@ +ids = $ids; + return $this; + } + + public function withTargetPHIDs(array $phids) { + $this->targetPHIDs = $phids; + return $this; + } + + public function withConsumed($consumed) { + $this->consumed = $consumed; + return $this; + } + + public function newResultObject() { + return new DrydockCommand(); + } + + protected function loadPage() { + return $this->loadStandardPage($this->newResultObject()); + } + + protected function willFilterPage(array $commands) { + $target_phids = mpull($commands, 'getTargetPHID'); + + $targets = id(new PhabricatorObjectQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withPHIDs($target_phids) + ->execute(); + $targets = mpull($targets, null, 'getPHID'); + + foreach ($commands as $key => $command) { + $target = idx($targets, $command->getTargetPHID()); + if (!$target) { + $this->didRejectResult($command); + unset($commands[$key]); + continue; + } + $command->attachCommandTarget($target); + } + + return $commands; + } + + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); + + if ($this->ids !== null) { + $where[] = qsprintf( + $conn, + 'id IN (%Ld)', + $this->ids); + } + + if ($this->targetPHIDs !== null) { + $where[] = qsprintf( + $conn, + 'targetPHID IN (%Ls)', + $this->targetPHIDs); + } + + if ($this->consumed !== null) { + $where[] = qsprintf( + $conn, + 'isConsumed = %d', + (int)$this->consumed); + } + + return $where; + } + +} diff --git a/src/applications/drydock/query/DrydockLeaseQuery.php b/src/applications/drydock/query/DrydockLeaseQuery.php index f7adc07cce..a212a27ca8 100644 --- a/src/applications/drydock/query/DrydockLeaseQuery.php +++ b/src/applications/drydock/query/DrydockLeaseQuery.php @@ -7,6 +7,7 @@ final class DrydockLeaseQuery extends DrydockQuery { private $resourceIDs; private $statuses; private $datasourceQuery; + private $needCommands; public function withIDs(array $ids) { $this->ids = $ids; diff --git a/src/applications/drydock/storage/DrydockCommand.php b/src/applications/drydock/storage/DrydockCommand.php new file mode 100644 index 0000000000..60cb363ecb --- /dev/null +++ b/src/applications/drydock/storage/DrydockCommand.php @@ -0,0 +1,69 @@ +setAuthorPHID($author->getPHID()) + ->setIsConsumed(0); + } + + protected function getConfiguration() { + return array( + self::CONFIG_COLUMN_SCHEMA => array( + 'command' => 'text32', + 'isConsumed' => 'bool', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_target' => array( + 'columns' => array('targetPHID', 'isConsumed'), + ), + ), + ) + parent::getConfiguration(); + } + + public function attachCommandTarget($target) { + $this->commandTarget = $target; + return $this; + } + + public function getCommandTarget() { + return $this->assertAttached($this->commandTarget); + } + + +/* -( PhabricatorPolicyInterface )----------------------------------------- */ + + + public function getCapabilities() { + return array( + PhabricatorPolicyCapability::CAN_VIEW, + ); + } + + public function getPolicy($capability) { + return $this->getCommandTarget()->getPolicy($capability); + } + + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + return $this->getCommandTarget()->hasAutomaticCapability( + $capability, + $viewer); + } + + public function describeAutomaticCapability($capability) { + return pht('Drydock commands have the same policies as their targets.'); + } + +} diff --git a/src/applications/drydock/storage/DrydockLease.php b/src/applications/drydock/storage/DrydockLease.php index 475d209ea0..bcdd008f68 100644 --- a/src/applications/drydock/storage/DrydockLease.php +++ b/src/applications/drydock/storage/DrydockLease.php @@ -30,11 +30,24 @@ final class DrydockLease extends DrydockDAO } public function __destruct() { - if ($this->releaseOnDestruction) { - if ($this->isActive()) { - $this->release(); - } + if (!$this->releaseOnDestruction) { + return; } + + if (!$this->canRelease()) { + return; + } + + $actor = PhabricatorUser::getOmnipotentUser(); + $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID(); + + $command = DrydockCommand::initializeNewCommand($actor) + ->setTargetPHID($this->getPHID()) + ->setAuthorPHID($drydock_phid) + ->setCommand(DrydockCommand::COMMAND_RELEASE) + ->save(); + + $this->scheduleUpdate(); } public function getLeaseName() { @@ -130,18 +143,6 @@ final class DrydockLease extends DrydockDAO return $this; } - public function release() { - $this->assertActive(); - $this->setStatus(DrydockLeaseStatus::STATUS_RELEASED); - $this->save(); - - DrydockSlotLock::releaseLocks($this->getPHID()); - - $this->resource = null; - - return $this; - } - public function isActive() { switch ($this->status) { case DrydockLeaseStatus::STATUS_ACQUIRED: @@ -262,6 +263,10 @@ final class DrydockLease extends DrydockDAO $this->isAcquired = true; + if ($new_status == DrydockLeaseStatus::STATUS_ACTIVE) { + $this->didActivate(); + } + return $this; } @@ -301,6 +306,8 @@ final class DrydockLease extends DrydockDAO $this->isActivated = true; + $this->didActivate(); + return $this; } @@ -308,6 +315,48 @@ final class DrydockLease extends DrydockDAO return $this->isActivated; } + public function canRelease() { + if (!$this->getID()) { + return false; + } + + switch ($this->getStatus()) { + case DrydockLeaseStatus::STATUS_RELEASED: + return false; + default: + return true; + } + } + + public function scheduleUpdate() { + PhabricatorWorker::scheduleTask( + 'DrydockLeaseUpdateWorker', + array( + 'leasePHID' => $this->getPHID(), + ), + array( + 'objectPHID' => $this->getPHID(), + )); + } + + private function didActivate() { + $viewer = PhabricatorUser::getOmnipotentUser(); + $need_update = false; + + $commands = id(new DrydockCommandQuery()) + ->setViewer($viewer) + ->withTargetPHIDs(array($this->getPHID())) + ->withConsumed(false) + ->execute(); + if ($commands) { + $need_update = true; + } + + if ($need_update) { + $this->scheduleUpdate(); + } + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -315,6 +364,7 @@ final class DrydockLease extends DrydockDAO public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, ); } @@ -322,6 +372,9 @@ final class DrydockLease extends DrydockDAO if ($this->getResource()) { return $this->getResource()->getPolicy($capability); } + + // TODO: Implement reasonable policies. + return PhabricatorPolicies::getMostOpenPolicy(); } diff --git a/src/applications/drydock/storage/DrydockResource.php b/src/applications/drydock/storage/DrydockResource.php index 6affb7863c..fefb6e7516 100644 --- a/src/applications/drydock/storage/DrydockResource.php +++ b/src/applications/drydock/storage/DrydockResource.php @@ -170,43 +170,44 @@ final class DrydockResource extends DrydockDAO return $this->isActivated; } - public function closeResource() { + public function canRelease() { + switch ($this->getStatus()) { + case DrydockResourceStatus::STATUS_CLOSED: + case DrydockResourceStatus::STATUS_DESTROYED: + return false; + default: + return true; + } + } - // TODO: This is super broken and will race other lease writers! + public function scheduleUpdate() { + PhabricatorWorker::scheduleTask( + 'DrydockResourceUpdateWorker', + array( + 'resourcePHID' => $this->getPHID(), + ), + array( + 'objectPHID' => $this->getPHID(), + )); + } - $this->openTransaction(); - $statuses = array( - DrydockLeaseStatus::STATUS_PENDING, - DrydockLeaseStatus::STATUS_ACTIVE, - ); + private function didActivate() { + $viewer = PhabricatorUser::getOmnipotentUser(); - $leases = id(new DrydockLeaseQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withResourceIDs(array($this->getID())) - ->withStatuses($statuses) - ->execute(); + $need_update = false; - foreach ($leases as $lease) { - switch ($lease->getStatus()) { - case DrydockLeaseStatus::STATUS_PENDING: - $message = pht('Breaking pending lease (resource closing).'); - $lease->setStatus(DrydockLeaseStatus::STATUS_BROKEN); - break; - case DrydockLeaseStatus::STATUS_ACTIVE: - $message = pht('Releasing active lease (resource closing).'); - $lease->setStatus(DrydockLeaseStatus::STATUS_RELEASED); - break; - } - DrydockBlueprintImplementation::writeLog($this, $lease, $message); - $lease->save(); - } + $commands = id(new DrydockCommandQuery()) + ->setViewer($viewer) + ->withTargetPHIDs(array($this->getPHID())) + ->withConsumed(false) + ->execute(); + if ($commands) { + $need_update = true; + } - $this->setStatus(DrydockResourceStatus::STATUS_CLOSED); - $this->save(); - - DrydockSlotLock::releaseLocks($this->getPHID()); - - $this->saveTransaction(); + if ($need_update) { + $this->scheduleUpdate(); + } } @@ -216,12 +217,15 @@ final class DrydockResource extends DrydockDAO public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { switch ($capability) { case PhabricatorPolicyCapability::CAN_VIEW: + case PhabricatorPolicyCapability::CAN_EDIT: + // TODO: Implement reasonable policies. return PhabricatorPolicies::getMostOpenPolicy(); } } diff --git a/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php b/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php new file mode 100644 index 0000000000..5e96a608f6 --- /dev/null +++ b/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php @@ -0,0 +1,60 @@ +getTaskDataValue('leasePHID'); + + $hash = PhabricatorHash::digestForIndex($lease_phid); + $lock_key = 'drydock.lease:'.$hash; + + $lock = PhabricatorGlobalLock::newLock($lock_key) + ->lock(1); + + $lease = $this->loadLease($lease_phid); + $this->updateLease($lease); + + $lock->unlock(); + } + + private function updateLease(DrydockLease $lease) { + $commands = $this->loadCommands($lease->getPHID()); + foreach ($commands as $command) { + if ($lease->getStatus() != DrydockLeaseStatus::STATUS_ACTIVE) { + // Leases can't receive commands before they activate or after they + // release. + break; + } + + $this->processCommand($lease, $command); + $command + ->setIsConsumed(true) + ->save(); + } + } + + private function processCommand( + DrydockLease $lease, + DrydockCommand $command) { + switch ($command->getCommand()) { + case DrydockCommand::COMMAND_RELEASE: + $this->releaseLease($lease); + break; + } + } + + private function releaseLease(DrydockLease $lease) { + $lease->openTransaction(); + $lease + ->setStatus(DrydockLeaseStatus::STATUS_RELEASED) + ->save(); + + // TODO: Hold slot locks until destruction? + DrydockSlotLock::releaseLocks($lease->getPHID()); + $lease->saveTransaction(); + + // TODO: Hook for resource release behaviors. + // TODO: Schedule lease destruction. + } + +} diff --git a/src/applications/drydock/worker/DrydockResourceUpdateWorker.php b/src/applications/drydock/worker/DrydockResourceUpdateWorker.php new file mode 100644 index 0000000000..78c5726b32 --- /dev/null +++ b/src/applications/drydock/worker/DrydockResourceUpdateWorker.php @@ -0,0 +1,92 @@ +getTaskDataValue('resourcePHID'); + + $hash = PhabricatorHash::digestForIndex($resource_phid); + $lock_key = 'drydock.resource:'.$hash; + + $lock = PhabricatorGlobalLock::newLock($lock_key) + ->lock(1); + + $resource = $this->loadResource($resource_phid); + $this->updateResource($resource); + + $lock->unlock(); + } + + private function updateResource(DrydockResource $resource) { + $commands = $this->loadCommands($resource->getPHID()); + foreach ($commands as $command) { + if ($resource->getStatus() != DrydockResourceStatus::STATUS_OPEN) { + // Resources can't receive commands before they activate or after they + // release. + break; + } + + $this->processCommand($resource, $command); + + $command + ->setIsConsumed(true) + ->save(); + } + } + + private function processCommand( + DrydockResource $resource, + DrydockCommand $command) { + + switch ($command->getCommand()) { + case DrydockCommand::COMMAND_RELEASE: + $this->releaseResource($resource); + break; + } + } + + private function releaseResource(DrydockResource $resource) { + if ($resource->getStatus() != DrydockResourceStatus::STATUS_OPEN) { + // If we had multiple release commands + // This command is only meaningful to resources in the "Open" state. + return; + } + + $viewer = $this->getViewer(); + $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID(); + + $resource->openTransaction(); + $resource + ->setStatus(DrydockResourceStatus::STATUS_CLOSED) + ->save(); + + // TODO: Hold slot locks until destruction? + DrydockSlotLock::releaseLocks($resource->getPHID()); + $resource->saveTransaction(); + + $statuses = array( + DrydockLeaseStatus::STATUS_PENDING, + DrydockLeaseStatus::STATUS_ACQUIRED, + DrydockLeaseStatus::STATUS_ACTIVE, + ); + + $leases = id(new DrydockLeaseQuery()) + ->setViewer($viewer) + ->withResourceIDs(array($resource->getID())) + ->withStatuses($statuses) + ->execute(); + + foreach ($leases as $lease) { + $command = DrydockCommand::initializeNewCommand($viewer) + ->setTargetPHID($lease->getPHID()) + ->setAuthorPHID($drydock_phid) + ->setCommand(DrydockCommand::COMMAND_RELEASE) + ->save(); + + $lease->scheduleUpdate(); + } + + // TODO: Schedule resource destruction. + } + +} diff --git a/src/applications/drydock/worker/DrydockWorker.php b/src/applications/drydock/worker/DrydockWorker.php index abff7bcd39..d41643de47 100644 --- a/src/applications/drydock/worker/DrydockWorker.php +++ b/src/applications/drydock/worker/DrydockWorker.php @@ -36,4 +36,18 @@ abstract class DrydockWorker extends PhabricatorWorker { return $resource; } + protected function loadCommands($target_phid) { + $viewer = $this->getViewer(); + + $commands = id(new DrydockCommandQuery()) + ->setViewer($viewer) + ->withTargetPHIDs(array($target_phid)) + ->withConsumed(false) + ->execute(); + + $commands = msort($commands, 'getID'); + + return $commands; + } + } diff --git a/src/applications/harbormaster/artifact/HarbormasterHostArtifact.php b/src/applications/harbormaster/artifact/HarbormasterHostArtifact.php index 73d00844af..ce109eb498 100644 --- a/src/applications/harbormaster/artifact/HarbormasterHostArtifact.php +++ b/src/applications/harbormaster/artifact/HarbormasterHostArtifact.php @@ -62,13 +62,16 @@ final class HarbormasterHostArtifact extends HarbormasterArtifact { public function releaseArtifact(PhabricatorUser $actor) { $lease = $this->loadArtifactLease($actor); - $resource = $lease->getResource(); - $blueprint = $resource->getBlueprint(); - - if ($lease->isActive()) { - $blueprint->releaseLease($resource, $lease); + if (!$lease->canRelease()) { + return; } + + $command = DrydockCommand::initializeNewCommand($actor) + ->setTargetPHID($lease->getPHID()) + ->setCommand(DrydockCommand::COMMAND_RELEASE) + ->save(); + + $lease->scheduleUpdate(); } - }