diff --git a/resources/celerity/map.php b/resources/celerity/map.php index f1a20bf002..7c927cd290 100644 --- a/resources/celerity/map.php +++ b/resources/celerity/map.php @@ -7,7 +7,7 @@ */ return array( 'names' => array( - 'core.pkg.css' => '5eabac59', + 'core.pkg.css' => 'a11c3643', 'core.pkg.js' => '47dc9ebb', 'darkconsole.pkg.js' => 'e7393ebb', 'differential.pkg.css' => '2de124c9', @@ -25,7 +25,7 @@ return array( 'rsrc/css/aphront/notification.css' => '9c279160', 'rsrc/css/aphront/panel-view.css' => '8427b78d', 'rsrc/css/aphront/phabricator-nav-view.css' => 'a24cb589', - 'rsrc/css/aphront/table-view.css' => '34ee903e', + 'rsrc/css/aphront/table-view.css' => '63985f5b', 'rsrc/css/aphront/tokenizer.css' => '04875312', 'rsrc/css/aphront/tooltip.css' => '7672b60f', 'rsrc/css/aphront/typeahead-browse.css' => 'd8581d2c', @@ -104,7 +104,7 @@ return array( 'rsrc/css/application/tokens/tokens.css' => '3d0f239e', 'rsrc/css/application/uiexample/example.css' => '528b19de', 'rsrc/css/core/core.css' => 'a76cefc9', - 'rsrc/css/core/remarkup.css' => 'e27a26b2', + 'rsrc/css/core/remarkup.css' => 'fa3a8225', 'rsrc/css/core/syntax.css' => '9fd11da8', 'rsrc/css/core/z-index.css' => '57ddcaa2', 'rsrc/css/diviner/diviner-shared.css' => '5a337049', @@ -492,7 +492,7 @@ return array( 'aphront-list-filter-view-css' => '5d6f0526', 'aphront-multi-column-view-css' => 'fd18389d', 'aphront-panel-view-css' => '8427b78d', - 'aphront-table-view-css' => '34ee903e', + 'aphront-table-view-css' => '63985f5b', 'aphront-tokenizer-control-css' => '04875312', 'aphront-tooltip-css' => '7672b60f', 'aphront-typeahead-control-css' => '0e403212', @@ -733,7 +733,7 @@ return array( 'phabricator-object-selector-css' => '85ee8ce6', 'phabricator-phtize' => 'd254d646', 'phabricator-prefab' => '6920d200', - 'phabricator-remarkup-css' => 'e27a26b2', + 'phabricator-remarkup-css' => 'fa3a8225', 'phabricator-search-results-css' => '7dea472c', 'phabricator-shaped-request' => '7cbe244b', 'phabricator-side-menu-view-css' => 'bec2458e', diff --git a/resources/sql/autopatches/20141222.maniphestprojtxn.php b/resources/sql/autopatches/20141222.maniphestprojtxn.php index ba7a8f2f1f..31aae8b986 100644 --- a/resources/sql/autopatches/20141222.maniphestprojtxn.php +++ b/resources/sql/autopatches/20141222.maniphestprojtxn.php @@ -9,41 +9,53 @@ $metadata = array( 'edge:type' => PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, ); foreach (new LiskMigrationIterator($table) as $txn) { - // ManiphestTransaction::TYPE_PROJECTS - if ($txn->getTransactionType() == 'projects') { - $old_value = mig20141222_build_edge_data( - $txn->getOldValue(), - $txn->getObjectPHID()); - $new_value = mig20141222_build_edge_data( - $txn->getNewvalue(), - $txn->getObjectPHID()); - queryfx( - $conn_w, - 'UPDATE %T SET '. - 'transactionType = %s, oldValue = %s, newValue = %s, metaData = %s '. - 'WHERE id = %d', - $table->getTableName(), - PhabricatorTransactions::TYPE_EDGE, - json_encode($old_value), - json_encode($new_value), - json_encode($metadata), - $txn->getID()); + if ($txn->getTransactionType() != 'projects') { + continue; } + + $old_value = mig20141222_build_edge_data( + $txn->getOldValue(), + $txn->getObjectPHID()); + + $new_value = mig20141222_build_edge_data( + $txn->getNewValue(), + $txn->getObjectPHID()); + + queryfx( + $conn_w, + 'UPDATE %T SET '. + 'transactionType = %s, oldValue = %s, newValue = %s, metaData = %s '. + 'WHERE id = %d', + $table->getTableName(), + PhabricatorTransactions::TYPE_EDGE, + json_encode($old_value), + json_encode($new_value), + json_encode($metadata), + $txn->getID()); } echo pht('Done.')."\n"; -function mig20141222_build_edge_data(array $project_phids, $task_phid) { +function mig20141222_build_edge_data($project_phids, $task_phid) { $edge_data = array(); + + // See T9464. If we didn't get a proper array value out of the transaction, + // just return an empty value so we can move forward. + if (!is_array($project_phids)) { + return $edge_data; + } + foreach ($project_phids as $project_phid) { if (!is_scalar($project_phid)) { continue; } + $edge_data[$project_phid] = array( 'src' => $task_phid, 'type' => PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, 'dst' => $project_phid, ); } + return $edge_data; } diff --git a/resources/sql/autopatches/20150916.drydock.slotlocks.1.sql b/resources/sql/autopatches/20150916.drydock.slotlocks.1.sql new file mode 100644 index 0000000000..837566f1bf --- /dev/null +++ b/resources/sql/autopatches/20150916.drydock.slotlocks.1.sql @@ -0,0 +1,8 @@ +CREATE TABLE {$NAMESPACE}_drydock.drydock_slotlock ( + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + ownerPHID VARBINARY(64) NOT NULL, + lockIndex BINARY(12) NOT NULL, + lockKey LONGTEXT NOT NULL COLLATE {$COLLATE_TEXT}, + UNIQUE KEY `key_lock` (lockIndex), + KEY `key_owner` (ownerPHID) +) ENGINE=InnoDB, COLLATE {$COLLATE_TEXT}; 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/resources/sql/autopatches/20150923.drydock.resourceid.1.sql b/resources/sql/autopatches/20150923.drydock.resourceid.1.sql new file mode 100644 index 0000000000..ad87d64669 --- /dev/null +++ b/resources/sql/autopatches/20150923.drydock.resourceid.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_drydock.drydock_lease + ADD resourcePHID VARBINARY(64); diff --git a/resources/sql/autopatches/20150923.drydock.resourceid.2.sql b/resources/sql/autopatches/20150923.drydock.resourceid.2.sql new file mode 100644 index 0000000000..22f6d32d47 --- /dev/null +++ b/resources/sql/autopatches/20150923.drydock.resourceid.2.sql @@ -0,0 +1,5 @@ +UPDATE + {$NAMESPACE}_drydock.drydock_lease l, + {$NAMESPACE}_drydock.drydock_resource r + SET l.resourcePHID = r.phid + WHERE l.resourceID = r.id; diff --git a/resources/sql/autopatches/20150923.drydock.resourceid.3.sql b/resources/sql/autopatches/20150923.drydock.resourceid.3.sql new file mode 100644 index 0000000000..f3520fa510 --- /dev/null +++ b/resources/sql/autopatches/20150923.drydock.resourceid.3.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_drydock.drydock_lease + DROP resourceID; diff --git a/resources/sql/autopatches/20150923.drydock.taskid.1.sql b/resources/sql/autopatches/20150923.drydock.taskid.1.sql new file mode 100644 index 0000000000..cbd6a1dc02 --- /dev/null +++ b/resources/sql/autopatches/20150923.drydock.taskid.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_drydock.drydock_lease + DROP taskID; diff --git a/resources/sql/autopatches/20150924.drydock.disable.1.sql b/resources/sql/autopatches/20150924.drydock.disable.1.sql new file mode 100644 index 0000000000..6e5dafe5ec --- /dev/null +++ b/resources/sql/autopatches/20150924.drydock.disable.1.sql @@ -0,0 +1,2 @@ +ALTER TABLE {$NAMESPACE}_drydock.drydock_blueprint + ADD isDisabled BOOL NOT NULL; diff --git a/resources/sql/autopatches/20150924.drydock.status.1.sql b/resources/sql/autopatches/20150924.drydock.status.1.sql new file mode 100644 index 0000000000..dc8ec2bd22 --- /dev/null +++ b/resources/sql/autopatches/20150924.drydock.status.1.sql @@ -0,0 +1,39 @@ +ALTER TABLE {$NAMESPACE}_drydock.drydock_lease + CHANGE status status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}; + +UPDATE {$NAMESPACE}_drydock.drydock_lease + SET status = 'pending' WHERE status = '0'; + +UPDATE {$NAMESPACE}_drydock.drydock_lease + SET status = 'acquired' WHERE status = '5'; + +UPDATE {$NAMESPACE}_drydock.drydock_lease + SET status = 'active' WHERE status = '1'; + +UPDATE {$NAMESPACE}_drydock.drydock_lease + SET status = 'released' WHERE status = '2'; + +UPDATE {$NAMESPACE}_drydock.drydock_lease + SET status = 'broken' WHERE status = '3'; + +UPDATE {$NAMESPACE}_drydock.drydock_lease + SET status = 'destroyed' WHERE status = '4'; + + +ALTER TABLE {$NAMESPACE}_drydock.drydock_resource + CHANGE status status VARCHAR(32) NOT NULL COLLATE {$COLLATE_TEXT}; + +UPDATE {$NAMESPACE}_drydock.drydock_resource + SET status = 'pending' WHERE status = '0'; + +UPDATE {$NAMESPACE}_drydock.drydock_resource + SET status = 'active' WHERE status = '1'; + +UPDATE {$NAMESPACE}_drydock.drydock_resource + SET status = 'released' WHERE status = '2'; + +UPDATE {$NAMESPACE}_drydock.drydock_resource + SET status = 'broken' WHERE status = '3'; + +UPDATE {$NAMESPACE}_drydock.drydock_resource + SET status = 'destroyed' WHERE status = '4'; diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 6fba6e6dcc..064d80740a 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -44,6 +44,7 @@ phutil_register_library_map(array( 'AlmanacDeviceTransaction' => 'applications/almanac/storage/AlmanacDeviceTransaction.php', 'AlmanacDeviceTransactionQuery' => 'applications/almanac/query/AlmanacDeviceTransactionQuery.php', 'AlmanacDeviceViewController' => 'applications/almanac/controller/AlmanacDeviceViewController.php', + 'AlmanacDrydockPoolServiceType' => 'applications/almanac/servicetype/AlmanacDrydockPoolServiceType.php', 'AlmanacInterface' => 'applications/almanac/storage/AlmanacInterface.php', 'AlmanacInterfaceDatasource' => 'applications/almanac/typeahead/AlmanacInterfaceDatasource.php', 'AlmanacInterfaceEditController' => 'applications/almanac/controller/AlmanacInterfaceEditController.php', @@ -796,6 +797,7 @@ phutil_register_library_map(array( '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', @@ -803,6 +805,7 @@ phutil_register_library_map(array( 'DrydockBlueprintCreateController' => 'applications/drydock/controller/DrydockBlueprintCreateController.php', 'DrydockBlueprintCustomField' => 'applications/drydock/customfield/DrydockBlueprintCustomField.php', 'DrydockBlueprintDatasource' => 'applications/drydock/typeahead/DrydockBlueprintDatasource.php', + 'DrydockBlueprintDisableController' => 'applications/drydock/controller/DrydockBlueprintDisableController.php', 'DrydockBlueprintEditController' => 'applications/drydock/controller/DrydockBlueprintEditController.php', 'DrydockBlueprintEditor' => 'applications/drydock/editor/DrydockBlueprintEditor.php', 'DrydockBlueprintImplementation' => 'applications/drydock/blueprint/DrydockBlueprintImplementation.php', @@ -810,12 +813,13 @@ phutil_register_library_map(array( 'DrydockBlueprintListController' => 'applications/drydock/controller/DrydockBlueprintListController.php', 'DrydockBlueprintPHIDType' => 'applications/drydock/phid/DrydockBlueprintPHIDType.php', 'DrydockBlueprintQuery' => 'applications/drydock/query/DrydockBlueprintQuery.php', - 'DrydockBlueprintScopeGuard' => 'applications/drydock/util/DrydockBlueprintScopeGuard.php', 'DrydockBlueprintSearchEngine' => 'applications/drydock/query/DrydockBlueprintSearchEngine.php', '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', @@ -828,6 +832,7 @@ phutil_register_library_map(array( 'DrydockLease' => 'applications/drydock/storage/DrydockLease.php', 'DrydockLeaseController' => 'applications/drydock/controller/DrydockLeaseController.php', 'DrydockLeaseDatasource' => 'applications/drydock/typeahead/DrydockLeaseDatasource.php', + 'DrydockLeaseDestroyWorker' => 'applications/drydock/worker/DrydockLeaseDestroyWorker.php', 'DrydockLeaseListController' => 'applications/drydock/controller/DrydockLeaseListController.php', 'DrydockLeaseListView' => 'applications/drydock/view/DrydockLeaseListView.php', 'DrydockLeasePHIDType' => 'applications/drydock/phid/DrydockLeasePHIDType.php', @@ -835,35 +840,43 @@ 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', - 'DrydockLocalCommandInterface' => 'applications/drydock/interface/command/DrydockLocalCommandInterface.php', + 'DrydockLeaseWorker' => 'applications/drydock/worker/DrydockLeaseWorker.php', 'DrydockLog' => 'applications/drydock/storage/DrydockLog.php', 'DrydockLogController' => 'applications/drydock/controller/DrydockLogController.php', 'DrydockLogListController' => 'applications/drydock/controller/DrydockLogListController.php', 'DrydockLogListView' => 'applications/drydock/view/DrydockLogListView.php', 'DrydockLogQuery' => 'applications/drydock/query/DrydockLogQuery.php', 'DrydockLogSearchEngine' => 'applications/drydock/query/DrydockLogSearchEngine.php', - 'DrydockManagementCloseWorkflow' => 'applications/drydock/management/DrydockManagementCloseWorkflow.php', - 'DrydockManagementCreateResourceWorkflow' => 'applications/drydock/management/DrydockManagementCreateResourceWorkflow.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', - 'DrydockPreallocatedHostBlueprintImplementation' => 'applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.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', + 'DrydockResourceDestroyWorker' => 'applications/drydock/worker/DrydockResourceDestroyWorker.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', 'DrydockSSHCommandInterface' => 'applications/drydock/interface/command/DrydockSSHCommandInterface.php', + 'DrydockSlotLock' => 'applications/drydock/storage/DrydockSlotLock.php', + 'DrydockSlotLockException' => 'applications/drydock/exception/DrydockSlotLockException.php', 'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php', + 'DrydockWorker' => 'applications/drydock/worker/DrydockWorker.php', 'DrydockWorkingCopyBlueprintImplementation' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php', 'FeedConduitAPIMethod' => 'applications/feed/conduit/FeedConduitAPIMethod.php', 'FeedPublishConduitAPIMethod' => 'applications/feed/conduit/FeedPublishConduitAPIMethod.php', @@ -989,11 +1002,15 @@ phutil_register_library_map(array( 'HarbormasterController' => 'applications/harbormaster/controller/HarbormasterController.php', 'HarbormasterCreateArtifactConduitAPIMethod' => 'applications/harbormaster/conduit/HarbormasterCreateArtifactConduitAPIMethod.php', 'HarbormasterDAO' => 'applications/harbormaster/storage/HarbormasterDAO.php', + 'HarbormasterDrydockCommandBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterDrydockCommandBuildStepImplementation.php', + 'HarbormasterDrydockLeaseArtifact' => 'applications/harbormaster/artifact/HarbormasterDrydockLeaseArtifact.php', + 'HarbormasterExecFuture' => 'applications/harbormaster/future/HarbormasterExecFuture.php', 'HarbormasterExternalBuildStepGroup' => 'applications/harbormaster/stepgroup/HarbormasterExternalBuildStepGroup.php', 'HarbormasterFileArtifact' => 'applications/harbormaster/artifact/HarbormasterFileArtifact.php', 'HarbormasterHTTPRequestBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php', 'HarbormasterHostArtifact' => 'applications/harbormaster/artifact/HarbormasterHostArtifact.php', 'HarbormasterLeaseHostBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php', + 'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php', 'HarbormasterLintMessagesController' => 'applications/harbormaster/controller/HarbormasterLintMessagesController.php', 'HarbormasterLintPropertyView' => 'applications/harbormaster/view/HarbormasterLintPropertyView.php', 'HarbormasterManagePlansCapability' => 'applications/harbormaster/capability/HarbormasterManagePlansCapability.php', @@ -1034,6 +1051,7 @@ phutil_register_library_map(array( 'HarbormasterUploadArtifactBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterUploadArtifactBuildStepImplementation.php', 'HarbormasterWaitForPreviousBuildStepImplementation' => 'applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php', 'HarbormasterWorker' => 'applications/harbormaster/worker/HarbormasterWorker.php', + 'HarbormasterWorkingCopyArtifact' => 'applications/harbormaster/artifact/HarbormasterWorkingCopyArtifact.php', 'HeraldAction' => 'applications/herald/action/HeraldAction.php', 'HeraldActionGroup' => 'applications/herald/action/HeraldActionGroup.php', 'HeraldActionRecord' => 'applications/herald/storage/HeraldActionRecord.php', @@ -2492,6 +2510,7 @@ phutil_register_library_map(array( 'PhabricatorPasteListController' => 'applications/paste/controller/PhabricatorPasteListController.php', 'PhabricatorPastePastePHIDType' => 'applications/paste/phid/PhabricatorPastePastePHIDType.php', 'PhabricatorPasteQuery' => 'applications/paste/query/PhabricatorPasteQuery.php', + 'PhabricatorPasteRawController' => 'applications/paste/controller/PhabricatorPasteRawController.php', 'PhabricatorPasteRemarkupRule' => 'applications/paste/remarkup/PhabricatorPasteRemarkupRule.php', 'PhabricatorPasteSchemaSpec' => 'applications/paste/storage/PhabricatorPasteSchemaSpec.php', 'PhabricatorPasteSearchEngine' => 'applications/paste/query/PhabricatorPasteSearchEngine.php', @@ -2915,6 +2934,7 @@ phutil_register_library_map(array( 'PhabricatorStandardCustomField' => 'infrastructure/customfield/standard/PhabricatorStandardCustomField.php', 'PhabricatorStandardCustomFieldBool' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldBool.php', 'PhabricatorStandardCustomFieldCredential' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldCredential.php', + 'PhabricatorStandardCustomFieldDatasource' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldDatasource.php', 'PhabricatorStandardCustomFieldDate' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldDate.php', 'PhabricatorStandardCustomFieldHeader' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldHeader.php', 'PhabricatorStandardCustomFieldInt' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldInt.php', @@ -2924,6 +2944,7 @@ phutil_register_library_map(array( 'PhabricatorStandardCustomFieldRemarkup' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldRemarkup.php', 'PhabricatorStandardCustomFieldSelect' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldSelect.php', 'PhabricatorStandardCustomFieldText' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldText.php', + 'PhabricatorStandardCustomFieldTokenizer' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php', 'PhabricatorStandardCustomFieldUsers' => 'infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php', 'PhabricatorStandardPageView' => 'view/page/PhabricatorStandardPageView.php', 'PhabricatorStandardSelectCustomFieldDatasource' => 'infrastructure/customfield/datasource/PhabricatorStandardSelectCustomFieldDatasource.php', @@ -3666,6 +3687,7 @@ phutil_register_library_map(array( 'AlmanacDeviceTransaction' => 'PhabricatorApplicationTransaction', 'AlmanacDeviceTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'AlmanacDeviceViewController' => 'AlmanacDeviceController', + 'AlmanacDrydockPoolServiceType' => 'AlmanacServiceType', 'AlmanacInterface' => array( 'AlmanacDAO', 'PhabricatorPolicyInterface', @@ -4498,7 +4520,8 @@ phutil_register_library_map(array( 'DoorkeeperSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'DoorkeeperTagView' => 'AphrontView', 'DoorkeeperTagsController' => 'PhabricatorController', - 'DrydockAllocatorWorker' => 'PhabricatorWorker', + 'DrydockAllocatorWorker' => 'DrydockWorker', + 'DrydockAlmanacServiceHostBlueprintImplementation' => 'DrydockBlueprintImplementation', 'DrydockApacheWebrootInterface' => 'DrydockWebrootInterface', 'DrydockBlueprint' => array( 'DrydockDAO', @@ -4514,6 +4537,7 @@ phutil_register_library_map(array( 'DrydockBlueprintCreateController' => 'DrydockBlueprintController', 'DrydockBlueprintCustomField' => 'PhabricatorCustomField', 'DrydockBlueprintDatasource' => 'PhabricatorTypeaheadDatasource', + 'DrydockBlueprintDisableController' => 'DrydockBlueprintController', 'DrydockBlueprintEditController' => 'DrydockBlueprintController', 'DrydockBlueprintEditor' => 'PhabricatorApplicationTransactionEditor', 'DrydockBlueprintImplementation' => 'Phobject', @@ -4521,12 +4545,16 @@ phutil_register_library_map(array( 'DrydockBlueprintListController' => 'DrydockBlueprintController', 'DrydockBlueprintPHIDType' => 'PhabricatorPHIDType', 'DrydockBlueprintQuery' => 'DrydockQuery', - 'DrydockBlueprintScopeGuard' => 'Phobject', 'DrydockBlueprintSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockBlueprintTransaction' => 'PhabricatorApplicationTransaction', 'DrydockBlueprintTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 'DrydockBlueprintViewController' => 'DrydockBlueprintController', + 'DrydockCommand' => array( + 'DrydockDAO', + 'PhabricatorPolicyInterface', + ), 'DrydockCommandInterface' => 'DrydockInterface', + 'DrydockCommandQuery' => 'DrydockQuery', 'DrydockConsoleController' => 'DrydockController', 'DrydockConstants' => 'Phobject', 'DrydockController' => 'PhabricatorController', @@ -4542,6 +4570,7 @@ phutil_register_library_map(array( ), 'DrydockLeaseController' => 'DrydockController', 'DrydockLeaseDatasource' => 'PhabricatorTypeaheadDatasource', + 'DrydockLeaseDestroyWorker' => 'DrydockWorker', 'DrydockLeaseListController' => 'DrydockLeaseController', 'DrydockLeaseListView' => 'AphrontView', 'DrydockLeasePHIDType' => 'PhabricatorPHIDType', @@ -4549,8 +4578,9 @@ phutil_register_library_map(array( 'DrydockLeaseReleaseController' => 'DrydockLeaseController', 'DrydockLeaseSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockLeaseStatus' => 'DrydockConstants', + 'DrydockLeaseUpdateWorker' => 'DrydockWorker', 'DrydockLeaseViewController' => 'DrydockLeaseController', - 'DrydockLocalCommandInterface' => 'DrydockCommandInterface', + 'DrydockLeaseWorker' => 'DrydockWorker', 'DrydockLog' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', @@ -4560,30 +4590,37 @@ phutil_register_library_map(array( 'DrydockLogListView' => 'AphrontView', 'DrydockLogQuery' => 'DrydockQuery', 'DrydockLogSearchEngine' => 'PhabricatorApplicationSearchEngine', - 'DrydockManagementCloseWorkflow' => 'DrydockManagementWorkflow', - 'DrydockManagementCreateResourceWorkflow' => 'DrydockManagementWorkflow', + 'DrydockManagementCommandWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementLeaseWorkflow' => 'DrydockManagementWorkflow', - 'DrydockManagementReleaseWorkflow' => 'DrydockManagementWorkflow', + 'DrydockManagementReleaseLeaseWorkflow' => 'DrydockManagementWorkflow', + 'DrydockManagementReleaseResourceWorkflow' => 'DrydockManagementWorkflow', + 'DrydockManagementUpdateLeaseWorkflow' => 'DrydockManagementWorkflow', + 'DrydockManagementUpdateResourceWorkflow' => 'DrydockManagementWorkflow', 'DrydockManagementWorkflow' => 'PhabricatorManagementWorkflow', - 'DrydockPreallocatedHostBlueprintImplementation' => 'DrydockBlueprintImplementation', 'DrydockQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DrydockResource' => array( 'DrydockDAO', 'PhabricatorPolicyInterface', ), - 'DrydockResourceCloseController' => 'DrydockResourceController', 'DrydockResourceController' => 'DrydockController', 'DrydockResourceDatasource' => 'PhabricatorTypeaheadDatasource', + 'DrydockResourceDestroyWorker' => 'DrydockWorker', 'DrydockResourceListController' => 'DrydockResourceController', 'DrydockResourceListView' => 'AphrontView', 'DrydockResourcePHIDType' => 'PhabricatorPHIDType', 'DrydockResourceQuery' => 'DrydockQuery', + 'DrydockResourceReleaseController' => 'DrydockResourceController', 'DrydockResourceSearchEngine' => 'PhabricatorApplicationSearchEngine', 'DrydockResourceStatus' => 'DrydockConstants', + 'DrydockResourceUpdateWorker' => 'DrydockWorker', 'DrydockResourceViewController' => 'DrydockResourceController', + 'DrydockResourceWorker' => 'DrydockWorker', 'DrydockSFTPFilesystemInterface' => 'DrydockFilesystemInterface', 'DrydockSSHCommandInterface' => 'DrydockCommandInterface', + 'DrydockSlotLock' => 'DrydockDAO', + 'DrydockSlotLockException' => 'Exception', 'DrydockWebrootInterface' => 'DrydockInterface', + 'DrydockWorker' => 'PhabricatorWorker', 'DrydockWorkingCopyBlueprintImplementation' => 'DrydockBlueprintImplementation', 'FeedConduitAPIMethod' => 'ConduitAPIMethod', 'FeedPublishConduitAPIMethod' => 'FeedConduitAPIMethod', @@ -4755,11 +4792,15 @@ phutil_register_library_map(array( 'HarbormasterController' => 'PhabricatorController', 'HarbormasterCreateArtifactConduitAPIMethod' => 'HarbormasterConduitAPIMethod', 'HarbormasterDAO' => 'PhabricatorLiskDAO', + 'HarbormasterDrydockCommandBuildStepImplementation' => 'HarbormasterBuildStepImplementation', + 'HarbormasterDrydockLeaseArtifact' => 'HarbormasterArtifact', + 'HarbormasterExecFuture' => 'Future', 'HarbormasterExternalBuildStepGroup' => 'HarbormasterBuildStepGroup', 'HarbormasterFileArtifact' => 'HarbormasterArtifact', 'HarbormasterHTTPRequestBuildStepImplementation' => 'HarbormasterBuildStepImplementation', - 'HarbormasterHostArtifact' => 'HarbormasterArtifact', + 'HarbormasterHostArtifact' => 'HarbormasterDrydockLeaseArtifact', 'HarbormasterLeaseHostBuildStepImplementation' => 'HarbormasterBuildStepImplementation', + 'HarbormasterLeaseWorkingCopyBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterLintMessagesController' => 'HarbormasterController', 'HarbormasterLintPropertyView' => 'AphrontView', 'HarbormasterManagePlansCapability' => 'PhabricatorPolicyCapability', @@ -4800,6 +4841,7 @@ phutil_register_library_map(array( 'HarbormasterUploadArtifactBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterWaitForPreviousBuildStepImplementation' => 'HarbormasterBuildStepImplementation', 'HarbormasterWorker' => 'PhabricatorWorker', + 'HarbormasterWorkingCopyArtifact' => 'HarbormasterDrydockLeaseArtifact', 'HeraldAction' => 'Phobject', 'HeraldActionGroup' => 'HeraldGroup', 'HeraldActionRecord' => 'HeraldDAO', @@ -6504,6 +6546,7 @@ phutil_register_library_map(array( 'PhabricatorPasteListController' => 'PhabricatorPasteController', 'PhabricatorPastePastePHIDType' => 'PhabricatorPHIDType', 'PhabricatorPasteQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', + 'PhabricatorPasteRawController' => 'PhabricatorPasteController', 'PhabricatorPasteRemarkupRule' => 'PhabricatorObjectRemarkupRule', 'PhabricatorPasteSchemaSpec' => 'PhabricatorConfigSchemaSpec', 'PhabricatorPasteSearchEngine' => 'PhabricatorApplicationSearchEngine', @@ -7022,6 +7065,7 @@ phutil_register_library_map(array( 'PhabricatorStandardCustomField' => 'PhabricatorCustomField', 'PhabricatorStandardCustomFieldBool' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldCredential' => 'PhabricatorStandardCustomField', + 'PhabricatorStandardCustomFieldDatasource' => 'PhabricatorStandardCustomFieldTokenizer', 'PhabricatorStandardCustomFieldDate' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldHeader' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldInt' => 'PhabricatorStandardCustomField', @@ -7030,7 +7074,8 @@ phutil_register_library_map(array( 'PhabricatorStandardCustomFieldRemarkup' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldSelect' => 'PhabricatorStandardCustomField', 'PhabricatorStandardCustomFieldText' => 'PhabricatorStandardCustomField', - 'PhabricatorStandardCustomFieldUsers' => 'PhabricatorStandardCustomFieldPHIDs', + 'PhabricatorStandardCustomFieldTokenizer' => 'PhabricatorStandardCustomFieldPHIDs', + 'PhabricatorStandardCustomFieldUsers' => 'PhabricatorStandardCustomFieldTokenizer', 'PhabricatorStandardPageView' => 'PhabricatorBarePageView', 'PhabricatorStandardSelectCustomFieldDatasource' => 'PhabricatorTypeaheadDatasource', 'PhabricatorStatusController' => 'PhabricatorController', diff --git a/src/applications/almanac/servicetype/AlmanacDrydockPoolServiceType.php b/src/applications/almanac/servicetype/AlmanacDrydockPoolServiceType.php new file mode 100644 index 0000000000..24880565d0 --- /dev/null +++ b/src/applications/almanac/servicetype/AlmanacDrydockPoolServiceType.php @@ -0,0 +1,18 @@ +withNamePrefix($raw_query) ->setOrder('name'); + // TODO: When service classes are restricted, it might be nice to customize + // the title and placeholder text to show which service types can be + // selected, or show all services but mark the invalid ones disabled and + // prevent their selection. + + $service_classes = $this->getParameter('serviceClasses'); + if ($service_classes) { + $services->withServiceClasses($service_classes); + } + $services = $this->executeQuery($services); if ($services) { diff --git a/src/applications/audit/conduit/AuditQueryConduitAPIMethod.php b/src/applications/audit/conduit/AuditQueryConduitAPIMethod.php index 2fc9ca47ee..97dbec3d68 100644 --- a/src/applications/audit/conduit/AuditQueryConduitAPIMethod.php +++ b/src/applications/audit/conduit/AuditQueryConduitAPIMethod.php @@ -37,7 +37,8 @@ final class AuditQueryConduitAPIMethod extends AuditConduitAPIMethod { protected function execute(ConduitAPIRequest $request) { $query = id(new DiffusionCommitQuery()) - ->setViewer($request->getUser()); + ->setViewer($request->getUser()) + ->needAuditRequests(true); $auditor_phids = $request->getValue('auditorPHIDs', array()); if ($auditor_phids) { diff --git a/src/applications/audit/mail/PhabricatorAuditMailReceiver.php b/src/applications/audit/mail/PhabricatorAuditMailReceiver.php index 29b1748264..36e68c76a9 100644 --- a/src/applications/audit/mail/PhabricatorAuditMailReceiver.php +++ b/src/applications/audit/mail/PhabricatorAuditMailReceiver.php @@ -8,11 +8,11 @@ final class PhabricatorAuditMailReceiver extends PhabricatorObjectMailReceiver { } protected function getObjectPattern() { - return 'C[1-9]\d*'; + return 'COMMIT[1-9]\d*'; } protected function loadObject($pattern, PhabricatorUser $viewer) { - $id = (int)trim($pattern, 'C'); + $id = (int)preg_replace('/^COMMIT/', '', $pattern); return id(new DiffusionCommitQuery()) ->setViewer($viewer) diff --git a/src/applications/audit/mail/PhabricatorAuditReplyHandler.php b/src/applications/audit/mail/PhabricatorAuditReplyHandler.php index c1eb562bfa..3b619fdd7f 100644 --- a/src/applications/audit/mail/PhabricatorAuditReplyHandler.php +++ b/src/applications/audit/mail/PhabricatorAuditReplyHandler.php @@ -13,9 +13,7 @@ final class PhabricatorAuditReplyHandler } public function getObjectPrefix() { - // TODO: This conflicts with Countdown and will probably need to be - // changed eventually. - return 'C'; + return 'COMMIT'; } } diff --git a/src/applications/badges/controller/PhabricatorBadgesEditController.php b/src/applications/badges/controller/PhabricatorBadgesEditController.php index 42f685bbf6..77ced98533 100644 --- a/src/applications/badges/controller/PhabricatorBadgesEditController.php +++ b/src/applications/badges/controller/PhabricatorBadgesEditController.php @@ -22,6 +22,9 @@ final class PhabricatorBadgesEditController } $is_new = false; } else { + $this->requireApplicationCapability( + PhabricatorBadgesCreateCapability::CAPABILITY); + $badge = PhabricatorBadgesBadge::initializeNewBadge($viewer); $is_new = true; } diff --git a/src/applications/badges/controller/PhabricatorBadgesViewController.php b/src/applications/badges/controller/PhabricatorBadgesViewController.php index 387b5d1f98..abaafe6d0f 100644 --- a/src/applications/badges/controller/PhabricatorBadgesViewController.php +++ b/src/applications/badges/controller/PhabricatorBadgesViewController.php @@ -109,7 +109,8 @@ final class PhabricatorBadgesViewController 'default', $viewer); - $view->addSectionHeader(pht('Description')); + $view->addSectionHeader( + pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $view->addTextContent($description); } diff --git a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php index 04d2c6ee6d..79312b5480 100644 --- a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php +++ b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php @@ -201,7 +201,8 @@ final class PhabricatorConduitConsoleController id(new PhabricatorMarkupOneOff())->setContent($description), 'default', $viewer); - $view->addSectionHeader(pht('Description')); + $view->addSectionHeader( + pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $view->addTextContent($description); return $view; diff --git a/src/applications/differential/storage/DifferentialDiff.php b/src/applications/differential/storage/DifferentialDiff.php index 7a5bc24203..25421bafe6 100644 --- a/src/applications/differential/storage/DifferentialDiff.php +++ b/src/applications/differential/storage/DifferentialDiff.php @@ -443,6 +443,7 @@ final class DifferentialDiff if ($repo) { $results['repository.callsign'] = $repo->getCallsign(); + $results['repository.phid'] = $repo->getPHID(); $results['repository.vcs'] = $repo->getVersionControlSystem(); $results['repository.uri'] = $repo->getPublicCloneURI(); } @@ -459,6 +460,8 @@ final class DifferentialDiff pht('The differential revision ID, if applicable.'), 'repository.callsign' => pht('The callsign of the repository in Phabricator.'), + 'repository.phid' => + pht('The PHID of the repository in Phabricator.'), 'repository.vcs' => pht('The version control system, either "svn", "hg" or "git".'), 'repository.uri' => diff --git a/src/applications/diffusion/controller/DiffusionBrowseController.php b/src/applications/diffusion/controller/DiffusionBrowseController.php index e8bd46b735..c924a9b0b3 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseController.php @@ -156,7 +156,8 @@ abstract class DiffusionBrowseController extends DiffusionController { $tag = idx($tags, $symbolic); if ($tag && strlen($tag->getMessage())) { - $view->addSectionHeader(pht('Tag Content')); + $view->addSectionHeader( + pht('Tag Content'), 'fa-tag'); $view->addTextContent($this->markupText($tag->getMessage())); } } diff --git a/src/applications/diffusion/controller/DiffusionBrowseFileController.php b/src/applications/diffusion/controller/DiffusionBrowseFileController.php index f975984789..fbd2785725 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseFileController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseFileController.php @@ -337,7 +337,8 @@ final class DiffusionBrowseFileController extends DiffusionBrowseController { $corpus = id(new PHUIObjectBoxView()) ->setHeader($header) - ->appendChild($corpus); + ->appendChild($corpus) + ->setCollapsed(true); return $corpus; } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryController.php b/src/applications/diffusion/controller/DiffusionRepositoryController.php index 5b63f16d71..cfa4f8f0d6 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryController.php @@ -289,7 +289,8 @@ final class DiffusionRepositoryController extends DiffusionController { $repository, 'description', $user); - $view->addSectionHeader(pht('Description')); + $view->addSectionHeader( + pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $view->addTextContent($description); } diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditMainController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditMainController.php index cd387537b0..6519a4380e 100644 --- a/src/applications/diffusion/controller/DiffusionRepositoryEditMainController.php +++ b/src/applications/diffusion/controller/DiffusionRepositoryEditMainController.php @@ -294,7 +294,8 @@ final class DiffusionRepositoryEditMainController $this->buildRepositoryUpdateInterval($repository)); $description = $repository->getDetail('description'); - $view->addSectionHeader(pht('Description')); + $view->addSectionHeader( + pht('Description'), PHUIPropertyListView::ICON_SUMMARY); if (!strlen($description)) { $description = phutil_tag('em', array(), pht('No description provided.')); } else { diff --git a/src/applications/drydock/application/PhabricatorDrydockApplication.php b/src/applications/drydock/application/PhabricatorDrydockApplication.php index 7919f1c9cf..5df54593ee 100644 --- a/src/applications/drydock/application/PhabricatorDrydockApplication.php +++ b/src/applications/drydock/application/PhabricatorDrydockApplication.php @@ -49,19 +49,31 @@ final class PhabricatorDrydockApplication extends PhabricatorApplication { '' => 'DrydockConsoleController', 'blueprint/' => array( '(?:query/(?P[^/]+)/)?' => 'DrydockBlueprintListController', - '(?P[1-9]\d*)/' => 'DrydockBlueprintViewController', + '(?P[1-9]\d*)/' => array( + '' => 'DrydockBlueprintViewController', + '(?Pdisable|enable)/' => + 'DrydockBlueprintDisableController', + 'resources/(?:query/(?P[^/]+)/)?' => + 'DrydockResourceListController', + ), 'create/' => 'DrydockBlueprintCreateController', 'edit/(?:(?P[1-9]\d*)/)?' => 'DrydockBlueprintEditController', ), 'resource/' => array( '(?:query/(?P[^/]+)/)?' => 'DrydockResourceListController', - '(?P[1-9]\d*)/' => 'DrydockResourceViewController', - '(?P[1-9]\d*)/close/' => 'DrydockResourceCloseController', + '(?P[1-9]\d*)/' => array( + '' => 'DrydockResourceViewController', + 'release/' => 'DrydockResourceReleaseController', + 'leases/(?:query/(?P[^/]+)/)?' => + 'DrydockLeaseListController', + ), ), 'lease/' => array( '(?:query/(?P[^/]+)/)?' => 'DrydockLeaseListController', - '(?P[1-9]\d*)/' => 'DrydockLeaseViewController', - '(?P[1-9]\d*)/release/' => 'DrydockLeaseReleaseController', + '(?P[1-9]\d*)/' => array( + '' => 'DrydockLeaseViewController', + 'release/' => 'DrydockLeaseReleaseController', + ), ), 'log/' => array( '(?:query/(?P[^/]+)/)?' => 'DrydockLogListController', diff --git a/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php new file mode 100644 index 0000000000..4ae64b39be --- /dev/null +++ b/src/applications/drydock/blueprint/DrydockAlmanacServiceHostBlueprintImplementation.php @@ -0,0 +1,289 @@ +loadServices($blueprint); + $bindings = $this->loadAllBindings($services); + + if (!$bindings) { + // If there are no devices bound to the services for this blueprint, + // we can not allocate resources. + return false; + } + + return true; + } + + public function canAllocateResourceForLease( + DrydockBlueprint $blueprint, + DrydockLease $lease) { + + // We will only allocate one resource per unique device bound to the + // services for this blueprint. Make sure we have a free device somewhere. + $free_bindings = $this->loadFreeBindings($blueprint); + if (!$free_bindings) { + return false; + } + + return true; + } + + public function allocateResource( + DrydockBlueprint $blueprint, + DrydockLease $lease) { + + $free_bindings = $this->loadFreeBindings($blueprint); + shuffle($free_bindings); + + $exceptions = array(); + foreach ($free_bindings as $binding) { + $device = $binding->getDevice(); + $device_name = $device->getName(); + + $binding_phid = $binding->getPHID(); + + $resource = $this->newResourceTemplate($blueprint, $device_name) + ->setActivateWhenAllocated(true) + ->setAttribute('almanacServicePHID', $binding->getServicePHID()) + ->setAttribute('almanacBindingPHID', $binding_phid) + ->needSlotLock("almanac.host.binding({$binding_phid})"); + + try { + return $resource->allocateResource(); + } catch (Exception $ex) { + $exceptions[] = $ex; + } + } + + throw new PhutilAggregateException( + pht('Unable to allocate any binding as a resource.'), + $exceptions); + } + + public function destroyResource( + DrydockBlueprint $blueprint, + DrydockResource $resource) { + // We don't create anything when allocating hosts, so we don't need to do + // any cleanup here. + return; + } + + public function canAcquireLeaseOnResource( + DrydockBlueprint $blueprint, + DrydockResource $resource, + DrydockLease $lease) { + + if (!DrydockSlotLock::isLockFree($this->getLeaseSlotLock($resource))) { + return false; + } + + return true; + } + + public function acquireLease( + DrydockBlueprint $blueprint, + DrydockResource $resource, + DrydockLease $lease) { + + $lease + ->setActivateWhenAcquired(true) + ->needSlotLock($this->getLeaseSlotLock($resource)) + ->acquireOnResource($resource); + } + + public function didReleaseLease( + DrydockBlueprint $blueprint, + DrydockResource $resource, + DrydockLease $lease) { + // Almanac hosts stick around indefinitely so we don't need to recycle them + // if they don't have any leases. + return; + } + + public function destroyLease( + DrydockBlueprint $blueprint, + DrydockResource $resource, + DrydockLease $lease) { + // We don't create anything when activating a lease, so we don't need to + // throw anything away. + return; + } + + private function getLeaseSlotLock(DrydockResource $resource) { + $resource_phid = $resource->getPHID(); + return "almanac.host.lease({$resource_phid})"; + } + + public function getType() { + return 'host'; + } + + public function getInterface( + DrydockBlueprint $blueprint, + DrydockResource $resource, + DrydockLease $lease, + $type) { + + $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() { + return array( + 'almanacServicePHIDs' => array( + 'name' => pht('Almanac Services'), + 'type' => 'datasource', + 'datasource.class' => 'AlmanacServiceDatasource', + 'datasource.parameters' => array( + 'serviceClasses' => $this->getAlmanacServiceClasses(), + ), + 'required' => true, + ), + 'credentialPHID' => array( + 'name' => pht('Credentials'), + 'type' => 'credential', + 'credential.provides' => + PassphraseSSHPrivateKeyCredentialType::PROVIDES_TYPE, + 'credential.type' => + PassphraseSSHPrivateKeyTextCredentialType::CREDENTIAL_TYPE, + ), + ) + parent::getFieldSpecifications(); + } + + private function loadServices(DrydockBlueprint $blueprint) { + if (!$this->services) { + $service_phids = $blueprint->getFieldValue('almanacServicePHIDs'); + if (!$service_phids) { + throw new Exception( + pht( + 'This blueprint ("%s") does not define any Almanac Service PHIDs.', + $blueprint->getBlueprintName())); + } + + $viewer = PhabricatorUser::getOmnipotentUser(); + $services = id(new AlmanacServiceQuery()) + ->setViewer($viewer) + ->withPHIDs($service_phids) + ->withServiceClasses($this->getAlmanacServiceClasses()) + ->needBindings(true) + ->execute(); + $services = mpull($services, null, 'getPHID'); + + if (count($services) != count($service_phids)) { + $missing_phids = array_diff($service_phids, array_keys($services)); + throw new Exception( + pht( + 'Some of the Almanac Services defined by this blueprint '. + 'could not be loaded. They may be invalid, no longer exist, '. + 'or be of the wrong type: %s.', + implode(', ', $missing_phids))); + } + + $this->services = $services; + } + + return $this->services; + } + + private function loadAllBindings(array $services) { + assert_instances_of($services, 'AlmanacService'); + $bindings = array_mergev(mpull($services, 'getBindings')); + return mpull($bindings, null, 'getPHID'); + } + + private function loadFreeBindings(DrydockBlueprint $blueprint) { + if ($this->freeBindings === null) { + $viewer = PhabricatorUser::getOmnipotentUser(); + + $pool = id(new DrydockResourceQuery()) + ->setViewer($viewer) + ->withBlueprintPHIDs(array($blueprint->getPHID())) + ->withStatuses( + array( + DrydockResourceStatus::STATUS_PENDING, + DrydockResourceStatus::STATUS_ACTIVE, + DrydockResourceStatus::STATUS_RELEASED, + )) + ->execute(); + + $allocated_phids = array(); + foreach ($pool as $resource) { + $allocated_phids[] = $resource->getAttribute('almanacDevicePHID'); + } + $allocated_phids = array_fuse($allocated_phids); + + $services = $this->loadServices($blueprint); + $bindings = $this->loadAllBindings($services); + + $free = array(); + foreach ($bindings as $binding) { + if (empty($allocated_phids[$binding->getPHID()])) { + $free[] = $binding; + } + } + + $this->freeBindings = $free; + } + + return $this->freeBindings; + } + + private function getAlmanacServiceClasses() { + return array( + 'AlmanacDrydockPoolServiceType', + ); + } + + +} diff --git a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php index 6a6e146cf1..f58767c4fc 100644 --- a/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockBlueprintImplementation.php @@ -1,328 +1,250 @@ 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() { return array(); } - public function getDetail($key, $default = null) { - return $this->getInstance()->getDetail($key, $default); - } - /* -( Lease Acquisition )-------------------------------------------------- */ - /** - * @task lease - */ - final public function filterResource( - DrydockResource $resource, - DrydockLease $lease) { - - $scope = $this->pushActiveScope($resource, $lease); - - return $this->canAllocateLease($resource, $lease); - } - - /** * Enforce basic checks on lease/resource compatibility. Allows resources to * reject leases if they are incompatible, even if the resource types match. * * For example, if a resource represents a 32-bit host, this method might - * reject leases that need a 64-bit host. If a resource represents a working - * copy of repository "X", this method might reject leases which need a - * working copy of repository "Y". Generally, although the main types of - * a lease and resource may match (e.g., both "host"), it may not actually be - * possible to satisfy the lease with a specific resource. + * reject leases that need a 64-bit host. The blueprint might also reject + * a resource if the lease needs 8GB of RAM and the resource only has 6GB + * free. * - * This method generally should not enforce limits or perform capacity - * checks. Perform those in @{method:shouldAllocateLease} instead. It also - * should not perform actual acquisition of the lease; perform that in - * @{method:executeAcquireLease} instead. + * This method should not acquire locks or expect anything to be locked. This + * is a coarse compatibility check between a lease and a resource. * - * @param DrydockResource Candidiate resource to allocate the lease on. - * @param DrydockLease Pending lease that wants to allocate here. - * @return bool True if the resource and lease are compatible. + * @param DrydockBlueprint Concrete blueprint to allocate for. + * @param DrydockResource Candidiate resource to allocate the lease on. + * @param DrydockLease Pending lease that wants to allocate here. + * @return bool True if the resource and lease are compatible. * @task lease */ - abstract protected function canAllocateLease( + abstract public function canAcquireLeaseOnResource( + DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease); /** - * @task lease - */ - final public function allocateLease( - DrydockResource $resource, - DrydockLease $lease) { - - $scope = $this->pushActiveScope($resource, $lease); - - $this->log(pht('Trying to Allocate Lease')); - - $lease->setStatus(DrydockLeaseStatus::STATUS_ACQUIRING); - $lease->setResourceID($resource->getID()); - $lease->attachResource($resource); - - $ephemeral_lease = id(clone $lease)->makeEphemeral(); - - $allocated = false; - $allocation_exception = null; - - $resource->openTransaction(); - $resource->beginReadLocking(); - $resource->reload(); - - // TODO: Policy stuff. - $other_leases = id(new DrydockLease())->loadAllWhere( - 'status IN (%Ld) AND resourceID = %d', - array( - DrydockLeaseStatus::STATUS_ACQUIRING, - DrydockLeaseStatus::STATUS_ACTIVE, - ), - $resource->getID()); - - try { - $allocated = $this->shouldAllocateLease( - $resource, - $ephemeral_lease, - $other_leases); - } catch (Exception $ex) { - $allocation_exception = $ex; - } - - if ($allocated) { - $lease->save(); - } - $resource->endReadLocking(); - if ($allocated) { - $resource->saveTransaction(); - $this->log(pht('Allocated Lease')); - } else { - $resource->killTransaction(); - $this->log(pht('Failed to Allocate Lease')); - } - - if ($allocation_exception) { - $this->logException($allocation_exception); - } - - return $allocated; - } - - - /** - * Enforce lease limits on resources. Allows resources to reject leases if - * they would become over-allocated by accepting them. - * - * For example, if a resource represents disk space, this method might check - * how much space the lease is asking for (say, 200MB) and how much space is - * left unallocated on the resource. It could grant the lease (return true) - * if it has enough remaining space (more than 200MB), and reject the lease - * (return false) if it does not (less than 200MB). - * - * A resource might also allow only exclusive leases. In this case it could - * accept a new lease (return true) if there are no active leases, or reject - * the new lease (return false) if there any other leases. - * - * A lock is held on the resource while this method executes to prevent - * multiple processes from allocating leases on the resource simultaneously. - * However, this means you should implement the method as cheaply as possible. - * In particular, do not perform any actual acquisition or setup in this - * method. - * - * If allocation is permitted, the lease will be moved to `ACQUIRING` status - * and @{method:executeAcquireLease} will be called to actually perform - * acquisition. - * - * General compatibility checks unrelated to resource limits and capacity are - * better implemented in @{method:canAllocateLease}, which serves as a - * cheap filter before lock acquisition. - * - * @param DrydockResource Candidate resource to allocate the lease on. - * @param DrydockLease Pending lease that wants to allocate here. - * @param list Other allocated and acquired leases on the - * resource. The implementation can inspect them - * to verify it can safely add the new lease. - * @return bool True to allocate the lease on the resource; - * false to reject it. - * @task lease - */ - abstract protected function shouldAllocateLease( - DrydockResource $resource, - DrydockLease $lease, - array $other_leases); - - - /** - * @task lease - */ - final public function acquireLease( - DrydockResource $resource, - DrydockLease $lease) { - - $scope = $this->pushActiveScope($resource, $lease); - - $this->log(pht('Acquiring Lease')); - $lease->setStatus(DrydockLeaseStatus::STATUS_ACTIVE); - $lease->setResourceID($resource->getID()); - $lease->attachResource($resource); - - $ephemeral_lease = id(clone $lease)->makeEphemeral(); - - try { - $this->executeAcquireLease($resource, $ephemeral_lease); - } catch (Exception $ex) { - $this->logException($ex); - throw $ex; - } - - $lease->setAttributes($ephemeral_lease->getAttributes()); - $lease->save(); - $this->log(pht('Acquired Lease')); - } - - - /** - * Acquire and activate an allocated lease. Allows resources to peform setup - * as leases are brought online. - * - * Following a successful call to @{method:canAllocateLease}, a lease is moved - * to `ACQUIRING` status and this method is called after resource locks are - * released. Nothing is locked while this method executes; the implementation - * is free to perform expensive operations like writing files and directories, - * executing commands, etc. - * - * After this method executes, the lease status is moved to `ACTIVE` and the - * original leasee may access it. + * Acquire a lease. Allows resources to peform setup as leases are brought + * online. * * If acquisition fails, throw an exception. * - * @param DrydockResource Resource to acquire a lease on. - * @param DrydockLease Lease to acquire. - * @return void + * @param DrydockBlueprint Blueprint which built the resource. + * @param DrydockResource Resource to acquire a lease on. + * @param DrydockLease Requested lease. + * @return void + * @task lease */ - abstract protected function executeAcquireLease( + abstract public function acquireLease( + DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease); - - final public function releaseLease( + /** + * @return void + * @task lease + */ + public function activateLease( + DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { - $scope = $this->pushActiveScope(null, $lease); - - $released = false; - - $lease->openTransaction(); - $lease->beginReadLocking(); - $lease->reload(); - - if ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACTIVE) { - $lease->setStatus(DrydockLeaseStatus::STATUS_RELEASED); - $lease->save(); - $released = true; - } - - $lease->endReadLocking(); - $lease->saveTransaction(); - - if (!$released) { - throw new Exception(pht('Unable to release lease: lease not active!')); - } - + throw new PhutilMethodNotImplementedException(); } + /** + * React to a lease being released. + * + * This callback is primarily useful for automatically releasing resources + * once all leases are released. + * + * @param DrydockBlueprint Blueprint which built the resource. + * @param DrydockResource Resource a lease was released on. + * @param DrydockLease Recently released lease. + * @return void + * @task lease + */ + abstract public function didReleaseLease( + DrydockBlueprint $blueprint, + DrydockResource $resource, + DrydockLease $lease); + + + /** + * Destroy any temporary data associated with a lease. + * + * If a lease creates temporary state while held, destroy it here. + * + * @param DrydockBlueprint Blueprint which built the resource. + * @param DrydockResource Resource the lease is acquired on. + * @param DrydockLease The lease being destroyed. + * @return void + * @task lease + */ + abstract public function destroyLease( + DrydockBlueprint $blueprint, + DrydockResource $resource, + DrydockLease $lease); + /* -( Resource Allocation )------------------------------------------------ */ - public function canAllocateMoreResources(array $pool) { - return true; + /** + * Enforce fundamental implementation/lease checks. Allows implementations to + * reject a lease which no concrete blueprint can ever satisfy. + * + * For example, if a lease only builds ARM hosts and the lease needs a + * PowerPC host, it may be rejected here. + * + * This is the earliest rejection phase, and followed by + * @{method:canEverAllocateResourceForLease}. + * + * This method should not actually check if a resource can be allocated + * right now, or even if a blueprint which can allocate a suitable resource + * really exists, only if some blueprint may conceivably exist which could + * plausibly be able to build a suitable resource. + * + * @param DrydockLease Requested lease. + * @return bool True if some concrete blueprint of this implementation's + * type might ever be able to build a resource for the lease. + * @task resource + */ + abstract public function canAnyBlueprintEverAllocateResourceForLease( + DrydockLease $lease); + + + /** + * Enforce basic blueprint/lease checks. Allows blueprints to reject a lease + * which they can not build a resource for. + * + * This is the second rejection phase. It follows + * @{method:canAnyBlueprintEverAllocateResourceForLease} and is followed by + * @{method:canAllocateResourceForLease}. + * + * This method should not check if a resource can be built right now, only + * if the blueprint as configured may, at some time, be able to build a + * suitable resource. + * + * @param DrydockBlueprint Blueprint which may be asked to allocate a + * resource. + * @param DrydockLease Requested lease. + * @return bool True if this blueprint can eventually build a suitable + * resource for the lease, as currently configured. + * @task resource + */ + abstract public function canEverAllocateResourceForLease( + DrydockBlueprint $blueprint, + DrydockLease $lease); + + + /** + * Enforce basic availability limits. Allows blueprints to reject resource + * allocation if they are currently overallocated. + * + * This method should perform basic capacity/limit checks. For example, if + * it has a limit of 6 resources and currently has 6 resources allocated, + * it might reject new leases. + * + * This method should not acquire locks or expect locks to be acquired. This + * is a coarse check to determine if the operation is likely to succeed + * right now without needing to acquire locks. + * + * It is expected that this method will sometimes return `true` (indicating + * that a resource can be allocated) but find that another allocator has + * eaten up free capacity by the time it actually tries to build a resource. + * This is normal and the allocator will recover from it. + * + * @param DrydockBlueprint The blueprint which may be asked to allocate a + * resource. + * @param DrydockLease Requested lease. + * @return bool True if this blueprint appears likely to be able to allocate + * a suitable resource. + * @task resource + */ + abstract public function canAllocateResourceForLease( + DrydockBlueprint $blueprint, + DrydockLease $lease); + + + /** + * Allocate a suitable resource for a lease. + * + * This method MUST acquire, hold, and manage locks to prevent multiple + * allocations from racing. World state is not locked before this method is + * called. Blueprints are entirely responsible for any lock handling they + * need to perform. + * + * @param DrydockBlueprint The blueprint which should allocate a resource. + * @param DrydockLease Requested lease. + * @return DrydockResource Allocated resource. + * @task resource + */ + abstract public function allocateResource( + DrydockBlueprint $blueprint, + DrydockLease $lease); + + + /** + * @task resource + */ + public function activateResource( + DrydockBlueprint $blueprint, + DrydockResource $resource) { + throw new PhutilMethodNotImplementedException(); } - abstract protected function executeAllocateResource(DrydockLease $lease); + + /** + * Destroy any temporary data associated with a resource. + * + * If a resource creates temporary state when allocated, destroy that state + * here. For example, you might shut down a virtual host or destroy a working + * copy on disk. + * + * @param DrydockBlueprint Blueprint which built the resource. + * @param DrydockResource Resource being destroyed. + * @return void + * @task resource + */ + abstract public function destroyResource( + DrydockBlueprint $blueprint, + DrydockResource $resource); - final public function allocateResource(DrydockLease $lease) { - $scope = $this->pushActiveScope(null, $lease); +/* -( Resource Interfaces )------------------------------------------------ */ - $this->log( - pht( - "Blueprint '%s': Allocating Resource for '%s'", - $this->getBlueprintClass(), - $lease->getLeaseName())); - try { - $resource = $this->executeAllocateResource($lease); - $this->validateAllocatedResource($resource); - } catch (Exception $ex) { - $this->logException($ex); - throw $ex; - } - - return $resource; - } + abstract public function getInterface( + DrydockBlueprint $blueprint, + DrydockResource $resource, + DrydockLease $lease, + $type); /* -( Logging )------------------------------------------------------------ */ @@ -340,10 +262,7 @@ abstract class DrydockBlueprintImplementation extends Phobject { * @task log */ protected function log($message) { - self::writeLog( - $this->activeResource, - $this->activeLease, - $message); + self::writeLog(null, null, $message); } @@ -377,88 +296,44 @@ abstract class DrydockBlueprintImplementation extends Phobject { ->execute(); } - public static function getAllBlueprintImplementationsForResource($type) { - static $groups = null; - if ($groups === null) { - $groups = mgroup(self::getAllBlueprintImplementations(), 'getType'); - } - return idx($groups, $type, array()); - } - public static function getNamedImplementation($class) { return idx(self::getAllBlueprintImplementations(), $class); } - protected function newResourceTemplate($name) { + protected function newResourceTemplate( + DrydockBlueprint $blueprint, + $name) { + $resource = id(new DrydockResource()) - ->setBlueprintPHID($this->getInstance()->getPHID()) - ->setBlueprintClass($this->getBlueprintClass()) + ->setBlueprintPHID($blueprint->getPHID()) + ->attachBlueprint($blueprint) ->setType($this->getType()) ->setStatus(DrydockResourceStatus::STATUS_PENDING) - ->setName($name) - ->save(); + ->setName($name); - $this->activeResource = $resource; - - $this->log( - pht( - "Blueprint '%s': Created New Template", - $this->getBlueprintClass())); + // Pre-allocate the resource PHID. + $resource->setPHID($resource->generatePHID()); return $resource; } - /** - * Sanity checks that the blueprint is implemented properly. - */ - private function validateAllocatedResource($resource) { - $blueprint = $this->getBlueprintClass(); - - if (!($resource instanceof DrydockResource)) { - throw new Exception( - pht( - "Blueprint '%s' is not properly implemented: %s must return an ". - "object of type %s or throw, but returned something else.", - $blueprint, - 'executeAllocateResource()', - 'DrydockResource')); - } - - $current_status = $resource->getStatus(); - $req_status = DrydockResourceStatus::STATUS_OPEN; - if ($current_status != $req_status) { - $current_name = DrydockResourceStatus::getNameForStatus($current_status); - $req_name = DrydockResourceStatus::getNameForStatus($req_status); - throw new Exception( - pht( - "Blueprint '%s' is not properly implemented: %s must return a %s ". - "with status '%s', but returned one with status '%s'.", - $blueprint, - 'executeAllocateResource()', - 'DrydockResource', - $req_name, - $current_name)); - } + protected function newLease(DrydockBlueprint $blueprint) { + return id(new DrydockLease()); } - private function pushActiveScope( - DrydockResource $resource = null, - DrydockLease $lease = null) { + protected function requireActiveLease(DrydockLease $lease) { + $lease_status = $lease->getStatus(); - if (($this->activeResource !== null) || - ($this->activeLease !== null)) { - throw new Exception(pht('There is already an active resource or lease!')); + switch ($lease_status) { + case DrydockLeaseStatus::STATUS_ACQUIRED: + // TODO: Temporary failure. + throw new Exception(pht('Lease still activating.')); + case DrydockLeaseStatus::STATUS_ACTIVE: + return; + default: + // TODO: Permanent failure. + throw new Exception(pht('Lease in bad state.')); } - - $this->activeResource = $resource; - $this->activeLease = $lease; - - return new DrydockBlueprintScopeGuard($this); - } - - public function popActiveScope() { - $this->activeResource = null; - $this->activeLease = null; } } diff --git a/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php b/src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php deleted file mode 100644 index 9e5d3a2320..0000000000 --- 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 index 264394f8ac..6ffe90469d 100644 --- a/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php +++ b/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php @@ -15,86 +15,190 @@ final class DrydockWorkingCopyBlueprintImplementation return pht('Allows Drydock to check out working copies of repositories.'); } - protected function canAllocateLease( + public function canAnyBlueprintEverAllocateResourceForLease( + DrydockLease $lease) { + return true; + } + + public function canEverAllocateResourceForLease( + DrydockBlueprint $blueprint, + DrydockLease $lease) { + return true; + } + + public function canAllocateResourceForLease( + DrydockBlueprint $blueprint, + DrydockLease $lease) { + return true; + } + + public function canAcquireLeaseOnResource( + DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { - $resource_repo = $resource->getAttribute('repositoryID'); - $lease_repo = $lease->getAttribute('repositoryID'); + $have_phid = $resource->getAttribute('repositoryPHID'); + $need_phid = $lease->getAttribute('repositoryPHID'); - return ($resource_repo && $lease_repo && ($resource_repo == $lease_repo)); + if ($need_phid !== $have_phid) { + return false; + } + + if (!DrydockSlotLock::isLockFree($this->getLeaseSlotLock($resource))) { + return false; + } + + return true; } - protected function shouldAllocateLease( + public function acquireLease( + DrydockBlueprint $blueprint, DrydockResource $resource, - DrydockLease $lease, - array $other_leases) { + DrydockLease $lease) { - return !$other_leases; + $lease + ->needSlotLock($this->getLeaseSlotLock($resource)) + ->acquireOnResource($resource); } - protected function executeAllocateResource(DrydockLease $lease) { - $repository_id = $lease->getAttribute('repositoryID'); - if (!$repository_id) { - throw new Exception( - pht( - "Lease is missing required '%s' attribute.", - 'repositoryID')); - } + private function getLeaseSlotLock(DrydockResource $resource) { + $resource_phid = $resource->getPHID(); + return "workingcopy.lease({$resource_phid})"; + } - $repository = id(new PhabricatorRepositoryQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withIDs(array($repository_id)) - ->executeOne(); + public function allocateResource( + DrydockBlueprint $blueprint, + DrydockLease $lease) { - if (!$repository) { - throw new Exception( - pht( - "Repository '%s' does not exist!", - $repository_id)); - } - - switch ($repository->getVersionControlSystem()) { - case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: - break; - default: - throw new Exception(pht('Unsupported VCS!')); - } - - // TODO: Policy stuff here too. - $host_lease = id(new DrydockLease()) - ->setResourceType('host') - ->waitUntilActive(); - - $path = $host_lease->getAttribute('path').$repository->getCallsign(); - - $this->log( - pht('Cloning %s into %s....', $repository->getCallsign(), $path)); - - $cmd = $host_lease->getInterface('command'); - $cmd->execx( - 'git clone --origin origin %P %s', - $repository->getRemoteURIEnvelope(), - $path); - - $this->log(pht('Complete.')); + $repository_phid = $lease->getAttribute('repositoryPHID'); + $repository = $this->loadRepository($repository_phid); $resource = $this->newResourceTemplate( + $blueprint, pht( 'Working Copy (%s)', $repository->getCallsign())); - $resource->setStatus(DrydockResourceStatus::STATUS_OPEN); - $resource->setAttribute('lease.host', $host_lease->getID()); - $resource->setAttribute('path', $path); - $resource->setAttribute('repositoryID', $repository->getID()); - $resource->save(); - return $resource; + $resource_phid = $resource->getPHID(); + + $host_lease = $this->newLease($blueprint) + ->setResourceType('host') + ->setOwnerPHID($resource_phid) + ->setAttribute('workingcopy.resourcePHID', $resource_phid) + ->queueForActivation(); + + // TODO: Add some limits to the number of working copies we can have at + // once? + + return $resource + ->setAttribute('repositoryPHID', $repository->getPHID()) + ->setAttribute('host.leasePHID', $host_lease->getPHID()) + ->allocateResource(); } - protected function executeAcquireLease( + public function activateResource( + DrydockBlueprint $blueprint, + DrydockResource $resource) { + + $lease = $this->loadHostLease($resource); + $this->requireActiveLease($lease); + + $repository_phid = $resource->getAttribute('repositoryPHID'); + $repository = $this->loadRepository($repository_phid); + $repository_id = $repository->getID(); + + $command_type = DrydockCommandInterface::INTERFACE_TYPE; + $interface = $lease->getInterface($command_type); + + // TODO: Make this configurable. + $resource_id = $resource->getID(); + $root = "/var/drydock/workingcopy-{$resource_id}"; + $path = "{$root}/repo/{$repository_id}/"; + + $interface->execx( + 'git clone -- %s %s', + (string)$repository->getCloneURIObject(), + $path); + + $resource + ->setAttribute('workingcopy.root', $root) + ->setAttribute('workingcopy.path', $path) + ->activateResource(); + } + + public function destroyResource( + DrydockBlueprint $blueprint, + DrydockResource $resource) { + + $lease = $this->loadHostLease($resource); + + // Destroy the lease on the host. + $lease->releaseOnDestruction(); + + // Destroy the working copy on disk. + $command_type = DrydockCommandInterface::INTERFACE_TYPE; + $interface = $lease->getInterface($command_type); + + $root_key = 'workingcopy.root'; + $root = $resource->getAttribute($root_key); + if (strlen($root)) { + $interface->execx('rm -rf -- %s', $root); + } + } + + public function activateLease( + DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease) { + + $command_type = DrydockCommandInterface::INTERFACE_TYPE; + $interface = $lease->getInterface($command_type); + + $cmd = array(); + $arg = array(); + + $cmd[] = 'git clean -d --force'; + $cmd[] = 'git reset --hard HEAD'; + $cmd[] = 'git fetch'; + + $commit = $lease->getAttribute('commit'); + $branch = $lease->getAttribute('branch'); + + if ($commit !== null) { + $cmd[] = 'git reset --hard %s'; + $arg[] = $commit; + } else if ($branch !== null) { + $cmd[] = 'git reset --hard %s'; + $arg[] = $branch; + } + + $cmd = implode(' && ', $cmd); + $argv = array_merge(array($cmd), $arg); + + $result = call_user_func_array( + array($interface, 'execx'), + $argv); + + $lease->activateOnResource($resource); + } + + public function didReleaseLease( + DrydockBlueprint $blueprint, + DrydockResource $resource, + DrydockLease $lease) { + // We leave working copies around even if there are no leases on them, + // since the cost to maintain them is nearly zero but rebuilding them is + // moderately expensive and it's likely that they'll be reused. + return; + } + + public function destroyLease( + DrydockBlueprint $blueprint, + DrydockResource $resource, + DrydockLease $lease) { + // When we activate a lease we just reset the working copy state and do + // not create any new state, so we don't need to do anything special when + // destroying a lease. return; } @@ -103,18 +207,63 @@ final class DrydockWorkingCopyBlueprintImplementation } public function getInterface( + DrydockBlueprint $blueprint, DrydockResource $resource, DrydockLease $lease, $type) { switch ($type) { - case 'command': - return $this - ->loadLease($resource->getAttribute('lease.host')) - ->getInterface($type); - } + case DrydockCommandInterface::INTERFACE_TYPE: + $host_lease = $this->loadHostLease($resource); + $command_interface = $host_lease->getInterface($type); - throw new Exception(pht("No interface of type '%s'.", $type)); + $path = $resource->getAttribute('workingcopy.path'); + $command_interface->setWorkingDirectory($path); + + return $command_interface; + } } + private function loadRepository($repository_phid) { + $repository = id(new PhabricatorRepositoryQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs(array($repository_phid)) + ->executeOne(); + if (!$repository) { + // TODO: Permanent failure. + throw new Exception( + pht( + 'Repository PHID "%s" does not exist.', + $repository_phid)); + } + + switch ($repository->getVersionControlSystem()) { + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: + break; + default: + // TODO: Permanent failure. + throw new Exception(pht('Unsupported VCS!')); + } + + return $repository; + } + + private function loadHostLease(DrydockResource $resource) { + $viewer = PhabricatorUser::getOmnipotentUser(); + + $lease_phid = $resource->getAttribute('host.leasePHID'); + + $lease = id(new DrydockLeaseQuery()) + ->setViewer($viewer) + ->withPHIDs(array($lease_phid)) + ->executeOne(); + if (!$lease) { + // TODO: Permanent failure. + throw new Exception(pht('Unable to load lease "%s".', $lease_phid)); + } + + return $lease; + } + + } diff --git a/src/applications/drydock/constants/DrydockLeaseStatus.php b/src/applications/drydock/constants/DrydockLeaseStatus.php index 847db52cbb..f37e4ab9be 100644 --- a/src/applications/drydock/constants/DrydockLeaseStatus.php +++ b/src/applications/drydock/constants/DrydockLeaseStatus.php @@ -2,35 +2,31 @@ final class DrydockLeaseStatus extends DrydockConstants { - const STATUS_PENDING = 0; - const STATUS_ACQUIRING = 5; - const STATUS_ACTIVE = 1; - const STATUS_RELEASED = 2; - const STATUS_BROKEN = 3; - const STATUS_EXPIRED = 4; + const STATUS_PENDING = 'pending'; + const STATUS_ACQUIRED = 'acquired'; + const STATUS_ACTIVE = 'active'; + const STATUS_RELEASED = 'released'; + const STATUS_BROKEN = 'broken'; + const STATUS_DESTROYED = 'destroyed'; + + public static function getStatusMap() { + return array( + self::STATUS_PENDING => pht('Pending'), + self::STATUS_ACQUIRED => pht('Acquired'), + self::STATUS_ACTIVE => pht('Active'), + self::STATUS_RELEASED => pht('Released'), + self::STATUS_BROKEN => pht('Broken'), + self::STATUS_DESTROYED => pht('Destroyed'), + ); + } public static function getNameForStatus($status) { - $map = array( - self::STATUS_PENDING => pht('Pending'), - self::STATUS_ACQUIRING => pht('Acquiring'), - self::STATUS_ACTIVE => pht('Active'), - self::STATUS_RELEASED => pht('Released'), - self::STATUS_BROKEN => pht('Broken'), - self::STATUS_EXPIRED => pht('Expired'), - ); - + $map = self::getStatusMap(); return idx($map, $status, pht('Unknown')); } public static function getAllStatuses() { - return array( - self::STATUS_PENDING, - self::STATUS_ACQUIRING, - self::STATUS_ACTIVE, - self::STATUS_RELEASED, - self::STATUS_BROKEN, - self::STATUS_EXPIRED, - ); + return array_keys(self::getStatusMap()); } } diff --git a/src/applications/drydock/constants/DrydockResourceStatus.php b/src/applications/drydock/constants/DrydockResourceStatus.php index d653138faf..d8a860d6a0 100644 --- a/src/applications/drydock/constants/DrydockResourceStatus.php +++ b/src/applications/drydock/constants/DrydockResourceStatus.php @@ -2,32 +2,29 @@ final class DrydockResourceStatus extends DrydockConstants { - const STATUS_PENDING = 0; - const STATUS_OPEN = 1; - const STATUS_CLOSED = 2; - const STATUS_BROKEN = 3; - const STATUS_DESTROYED = 4; + const STATUS_PENDING = 'pending'; + const STATUS_ACTIVE = 'active'; + const STATUS_RELEASED = 'released'; + const STATUS_BROKEN = 'broken'; + const STATUS_DESTROYED = 'destroyed'; + + public static function getStatusMap() { + return array( + self::STATUS_PENDING => pht('Pending'), + self::STATUS_ACTIVE => pht('Active'), + self::STATUS_RELEASED => pht('Released'), + self::STATUS_BROKEN => pht('Broken'), + self::STATUS_DESTROYED => pht('Destroyed'), + ); + } public static function getNameForStatus($status) { - $map = array( - self::STATUS_PENDING => pht('Pending'), - self::STATUS_OPEN => pht('Open'), - self::STATUS_CLOSED => pht('Closed'), - self::STATUS_BROKEN => pht('Broken'), - self::STATUS_DESTROYED => pht('Destroyed'), - ); - + $map = self::getStatusMap(); return idx($map, $status, pht('Unknown')); } public static function getAllStatuses() { - return array( - self::STATUS_PENDING, - self::STATUS_OPEN, - self::STATUS_CLOSED, - self::STATUS_BROKEN, - self::STATUS_DESTROYED, - ); + return array_keys(self::getStatusMap()); } } diff --git a/src/applications/drydock/controller/DrydockBlueprintDisableController.php b/src/applications/drydock/controller/DrydockBlueprintDisableController.php new file mode 100644 index 0000000000..525e55228b --- /dev/null +++ b/src/applications/drydock/controller/DrydockBlueprintDisableController.php @@ -0,0 +1,64 @@ +getViewer(); + $id = $request->getURIData('id'); + + $blueprint = id(new DrydockBlueprintQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) + ->executeOne(); + if (!$blueprint) { + return new Aphront404Response(); + } + + $is_disable = ($request->getURIData('action') == 'disable'); + $id = $blueprint->getID(); + $cancel_uri = $this->getApplicationURI("blueprint/{$id}/"); + + if ($request->isFormPost()) { + $xactions = array(); + + $xactions[] = id(new DrydockBlueprintTransaction()) + ->setTransactionType(DrydockBlueprintTransaction::TYPE_DISABLED) + ->setNewValue($is_disable ? 1 : 0); + + $editor = id(new DrydockBlueprintEditor()) + ->setActor($viewer) + ->setContentSourceFromRequest($request) + ->setContinueOnNoEffect(true) + ->setContinueOnMissingFields(true) + ->applyTransactions($blueprint, $xactions); + + return id(new AphrontRedirectResponse())->setURI($cancel_uri); + } + + if ($is_disable) { + $title = pht('Disable Blueprint'); + $body = pht( + 'If you disable this blueprint, Drydock will no longer use it to '. + 'allocate new resources. Existing resources will not be affected.'); + $button = pht('Disable Blueprint'); + } else { + $title = pht('Enable Blueprint'); + $body = pht( + 'If you enable this blueprint, Drydock will start using it to '. + 'allocate new resources.'); + $button = pht('Enable Blueprint'); + } + + return $this->newDialog() + ->setTitle($title) + ->appendParagraph($body) + ->addCancelButton($cancel_uri) + ->addSubmitButton($button); + } +} diff --git a/src/applications/drydock/controller/DrydockBlueprintEditController.php b/src/applications/drydock/controller/DrydockBlueprintEditController.php index 1c75bf708d..2e61ec868c 100644 --- a/src/applications/drydock/controller/DrydockBlueprintEditController.php +++ b/src/applications/drydock/controller/DrydockBlueprintEditController.php @@ -33,8 +33,10 @@ final class DrydockBlueprintEditController extends DrydockBlueprintController { return new Aphront400Response(); } - $blueprint = DrydockBlueprint::initializeNewBlueprint($viewer); - $blueprint->setClassName($class); + $blueprint = DrydockBlueprint::initializeNewBlueprint($viewer) + ->setClassName($class) + ->attachImplementation($impl); + $cancel_uri = $this->getApplicationURI('blueprint/'); } diff --git a/src/applications/drydock/controller/DrydockBlueprintViewController.php b/src/applications/drydock/controller/DrydockBlueprintViewController.php index 33a27264b8..6991e18fa2 100644 --- a/src/applications/drydock/controller/DrydockBlueprintViewController.php +++ b/src/applications/drydock/controller/DrydockBlueprintViewController.php @@ -21,27 +21,15 @@ final class DrydockBlueprintViewController extends DrydockBlueprintController { ->setUser($viewer) ->setPolicyObject($blueprint); + if ($blueprint->getIsDisabled()) { + $header->setStatus('fa-ban', 'red', pht('Disabled')); + } else { + $header->setStatus('fa-check', 'bluegrey', pht('Active')); + } + $actions = $this->buildActionListView($blueprint); $properties = $this->buildPropertyListView($blueprint, $actions); - $blueprint_uri = 'blueprint/'.$blueprint->getID().'/'; - $blueprint_uri = $this->getApplicationURI($blueprint_uri); - - $resources = id(new DrydockResourceQuery()) - ->withBlueprintPHIDs(array($blueprint->getPHID())) - ->setViewer($viewer) - ->execute(); - - $resource_list = id(new DrydockResourceListView()) - ->setUser($viewer) - ->setResources($resources) - ->render(); - $resource_list->setNoDataString(pht('This blueprint has no resources.')); - - $pager = new PHUIPagerView(); - $pager->setURI(new PhutilURI($blueprint_uri), 'offset'); - $pager->setOffset($request->getInt('offset')); - $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Blueprint %d', $blueprint->getID())); @@ -61,6 +49,8 @@ final class DrydockBlueprintViewController extends DrydockBlueprintController { $viewer, $properties); + $resource_box = $this->buildResourceBox($blueprint); + $timeline = $this->buildTransactionTimeline( $blueprint, new DrydockBlueprintTransactionQuery()); @@ -70,7 +60,7 @@ final class DrydockBlueprintViewController extends DrydockBlueprintController { array( $crumbs, $object_box, - $resource_list, + $resource_box, $timeline, ), array( @@ -80,15 +70,15 @@ final class DrydockBlueprintViewController extends DrydockBlueprintController { } private function buildActionListView(DrydockBlueprint $blueprint) { - $viewer = $this->getRequest()->getUser(); + $viewer = $this->getViewer(); + $id = $blueprint->getID(); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObjectURI($this->getRequest()->getRequestURI()) ->setObject($blueprint); - $uri = '/blueprint/edit/'.$blueprint->getID().'/'; - $uri = $this->getApplicationURI($uri); + $edit_uri = $this->getApplicationURI("blueprint/edit/{$id}/"); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, @@ -97,12 +87,30 @@ final class DrydockBlueprintViewController extends DrydockBlueprintController { $view->addAction( id(new PhabricatorActionView()) - ->setHref($uri) + ->setHref($edit_uri) ->setName(pht('Edit Blueprint')) ->setIcon('fa-pencil') ->setWorkflow(!$can_edit) ->setDisabled(!$can_edit)); + if (!$blueprint->getIsDisabled()) { + $disable_name = pht('Disable Blueprint'); + $disable_icon = 'fa-ban'; + $disable_uri = $this->getApplicationURI("blueprint/{$id}/disable/"); + } else { + $disable_name = pht('Enable Blueprint'); + $disable_icon = 'fa-check'; + $disable_uri = $this->getApplicationURI("blueprint/{$id}/enable/"); + } + + $view->addAction( + id(new PhabricatorActionView()) + ->setHref($disable_uri) + ->setName($disable_name) + ->setIcon($disable_icon) + ->setWorkflow(true) + ->setDisabled(!$can_edit)); + return $view; } @@ -120,4 +128,43 @@ final class DrydockBlueprintViewController extends DrydockBlueprintController { return $view; } + private function buildResourceBox(DrydockBlueprint $blueprint) { + $viewer = $this->getViewer(); + + $resources = id(new DrydockResourceQuery()) + ->setViewer($viewer) + ->withBlueprintPHIDs(array($blueprint->getPHID())) + ->withStatuses( + array( + DrydockResourceStatus::STATUS_PENDING, + DrydockResourceStatus::STATUS_ACTIVE, + )) + ->setLimit(100) + ->execute(); + + $resource_list = id(new DrydockResourceListView()) + ->setUser($viewer) + ->setResources($resources) + ->render() + ->setNoDataString(pht('This blueprint has no active resources.')); + + $id = $blueprint->getID(); + $resources_uri = "blueprint/{$id}/resources/query/all/"; + $resources_uri = $this->getApplicationURI($resources_uri); + + $resource_header = id(new PHUIHeaderView()) + ->setHeader(pht('Active Resources')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setHref($resources_uri) + ->setIconFont('fa-search') + ->setText(pht('View All Resources'))); + + return id(new PHUIObjectBoxView()) + ->setHeader($resource_header) + ->setObjectList($resource_list); + } + + } diff --git a/src/applications/drydock/controller/DrydockController.php b/src/applications/drydock/controller/DrydockController.php index c2fa3dae72..e0130bdf56 100644 --- a/src/applications/drydock/controller/DrydockController.php +++ b/src/applications/drydock/controller/DrydockController.php @@ -8,4 +8,81 @@ abstract class DrydockController extends PhabricatorController { return $this->buildSideNavView()->getMenu(); } + protected function buildLocksTab($owner_phid) { + $locks = DrydockSlotLock::loadLocks($owner_phid); + + $rows = array(); + foreach ($locks as $lock) { + $rows[] = array( + $lock->getID(), + $lock->getLockKey(), + ); + } + + $table = id(new AphrontTableView($rows)) + ->setNoDataString(pht('No slot locks held.')) + ->setHeaders( + array( + pht('ID'), + pht('Lock Key'), + )) + ->setColumnClasses( + array( + null, + 'wide', + )); + + return id(new PHUIPropertyListView()) + ->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/DrydockLeaseController.php b/src/applications/drydock/controller/DrydockLeaseController.php index d520fc66ee..d5a6335454 100644 --- a/src/applications/drydock/controller/DrydockLeaseController.php +++ b/src/applications/drydock/controller/DrydockLeaseController.php @@ -3,13 +3,29 @@ abstract class DrydockLeaseController extends DrydockController { + private $resource; + + public function setResource($resource) { + $this->resource = $resource; + return $this; + } + + public function getResource() { + return $this->resource; + } + public function buildSideNavView() { $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - id(new DrydockLeaseSearchEngine()) - ->setViewer($this->getRequest()->getUser()) - ->addNavigationItems($nav->getMenu()); + $engine = id(new DrydockLeaseSearchEngine()) + ->setViewer($this->getRequest()->getUser()); + + if ($this->getResource()) { + $engine->setResource($this->getResource()); + } + + $engine->addNavigationItems($nav->getMenu()); $nav->selectFilter(null); @@ -18,9 +34,28 @@ abstract class DrydockLeaseController protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); - $crumbs->addTextCrumb( - pht('Leases'), - $this->getApplicationURI('lease/')); + + $resource = $this->getResource(); + if ($resource) { + $id = $resource->getID(); + + $crumbs->addTextCrumb( + pht('Resources'), + $this->getApplicationURI('resource/')); + + $crumbs->addTextCrumb( + $resource->getName(), + $this->getApplicationURI("resource/{$id}/")); + + $crumbs->addTextCrumb( + pht('Leases'), + $this->getApplicationURI("resource/{$id}/leases/")); + + } else { + $crumbs->addTextCrumb( + pht('Leases'), + $this->getApplicationURI('lease/')); + } return $crumbs; } diff --git a/src/applications/drydock/controller/DrydockLeaseListController.php b/src/applications/drydock/controller/DrydockLeaseListController.php index e370467a5f..321e1d4ae7 100644 --- a/src/applications/drydock/controller/DrydockLeaseListController.php +++ b/src/applications/drydock/controller/DrydockLeaseListController.php @@ -8,11 +8,26 @@ final class DrydockLeaseListController extends DrydockLeaseController { public function handleRequest(AphrontRequest $request) { $viewer = $request->getViewer(); - $querykey = $request->getURIData('queryKey'); + $query_key = $request->getURIData('queryKey'); + + $engine = new DrydockLeaseSearchEngine(); + + $id = $request->getURIData('id'); + if ($id) { + $resource = id(new DrydockResourceQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$resource) { + return new Aphront404Response(); + } + $this->setResource($resource); + $engine->setResource($resource); + } $controller = id(new PhabricatorApplicationSearchController()) - ->setQueryKey($querykey) - ->setSearchEngine(new DrydockLeaseSearchEngine()) + ->setQueryKey($query_key) + ->setSearchEngine($engine) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); 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 92d215bcbb..bb748cceee 100644 --- a/src/applications/drydock/controller/DrydockLeaseViewController.php +++ b/src/applications/drydock/controller/DrydockLeaseViewController.php @@ -42,31 +42,46 @@ final class DrydockLeaseViewController extends DrydockLeaseController { $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($title, $lease_uri); + $locks = $this->buildLocksTab($lease->getPHID()); + $commands = $this->buildCommandsTab($lease->getPHID()); + $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) - ->addPropertyList($properties); + ->addPropertyList($properties, pht('Properties')) + ->addPropertyList($locks, pht('Slot Locks')) + ->addPropertyList($commands, pht('Commands')); + + $log_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Lease Logs')) + ->setTable($log_table); return $this->buildApplicationPage( array( $crumbs, $object_box, - $log_table, + $log_box, ), array( - 'title' => $title, + 'title' => $title, )); } 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()) @@ -74,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; } @@ -82,46 +97,39 @@ final class DrydockLeaseViewController extends DrydockLeaseController { private function buildPropertyListView( DrydockLease $lease, PhabricatorActionListView $actions) { + $viewer = $this->getViewer(); $view = new PHUIPropertyListView(); $view->setActionList($actions); - switch ($lease->getStatus()) { - case DrydockLeaseStatus::STATUS_ACTIVE: - $status = pht('Active'); - break; - case DrydockLeaseStatus::STATUS_RELEASED: - $status = pht('Released'); - break; - case DrydockLeaseStatus::STATUS_EXPIRED: - $status = pht('Expired'); - break; - case DrydockLeaseStatus::STATUS_PENDING: - $status = pht('Pending'); - break; - case DrydockLeaseStatus::STATUS_BROKEN: - $status = pht('Broken'); - break; - default: - $status = pht('Unknown'); - break; - } - $view->addProperty( pht('Status'), - $status); + DrydockLeaseStatus::getNameForStatus($lease->getStatus())); $view->addProperty( pht('Resource Type'), $lease->getResourceType()); - $view->addProperty( - pht('Resource'), - $lease->getResourceID()); + $resource_phid = $lease->getResourcePHID(); + if ($resource_phid) { + $resource_display = $viewer->renderHandle($resource_phid); + } else { + $resource_display = phutil_tag('em', array(), pht('No Resource')); + } + $view->addProperty(pht('Resource'), $resource_display); + + $until = $lease->getUntil(); + if ($until) { + $until_display = phabricator_datetime($until, $viewer); + } else { + $until_display = phutil_tag('em', array(), pht('Never')); + } + $view->addProperty(pht('Expires'), $until_display); $attributes = $lease->getAttributes(); if ($attributes) { - $view->addSectionHeader(pht('Attributes')); + $view->addSectionHeader( + pht('Attributes'), 'fa-list-ul'); foreach ($attributes as $key => $value) { $view->addProperty($key, $value); } 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/DrydockResourceController.php b/src/applications/drydock/controller/DrydockResourceController.php index e29b13e2c2..120d6d7cb0 100644 --- a/src/applications/drydock/controller/DrydockResourceController.php +++ b/src/applications/drydock/controller/DrydockResourceController.php @@ -3,13 +3,29 @@ abstract class DrydockResourceController extends DrydockController { + private $blueprint; + + public function setBlueprint($blueprint) { + $this->blueprint = $blueprint; + return $this; + } + + public function getBlueprint() { + return $this->blueprint; + } + public function buildSideNavView() { $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - id(new DrydockResourceSearchEngine()) - ->setViewer($this->getRequest()->getUser()) - ->addNavigationItems($nav->getMenu()); + $engine = id(new DrydockResourceSearchEngine()) + ->setViewer($this->getViewer()); + + if ($this->getBlueprint()) { + $engine->setBlueprint($this->getBlueprint()); + } + + $engine->addNavigationItems($nav->getMenu()); $nav->selectFilter(null); @@ -18,9 +34,26 @@ abstract class DrydockResourceController protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); - $crumbs->addTextCrumb( - pht('Resources'), - $this->getApplicationURI('resource/')); + + $blueprint = $this->getBlueprint(); + if ($blueprint) { + $id = $blueprint->getID(); + $crumbs->addTextCrumb( + pht('Blueprints'), + $this->getApplicationURI('blueprint/')); + + $crumbs->addTextCrumb( + $blueprint->getBlueprintName(), + $this->getApplicationURI("blueprint/{$id}/")); + + $crumbs->addTextCrumb( + pht('Resources'), + $this->getApplicationURI("blueprint/{$id}/resources/")); + } else { + $crumbs->addTextCrumb( + pht('Resources'), + $this->getApplicationURI('resource/')); + } return $crumbs; } diff --git a/src/applications/drydock/controller/DrydockResourceListController.php b/src/applications/drydock/controller/DrydockResourceListController.php index d2f34ec25b..6e1f80cbd6 100644 --- a/src/applications/drydock/controller/DrydockResourceListController.php +++ b/src/applications/drydock/controller/DrydockResourceListController.php @@ -7,12 +7,28 @@ final class DrydockResourceListController extends DrydockResourceController { } public function handleRequest(AphrontRequest $request) { - $viewer = $request->getViewer(); + $viewer = $this->getViewer(); + + $engine = new DrydockResourceSearchEngine(); + + $id = $request->getURIData('id'); + if ($id) { + $blueprint = id(new DrydockBlueprintQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$blueprint) { + return new Aphront404Response(); + } + $this->setBlueprint($blueprint); + $engine->setBlueprint($blueprint); + } + $querykey = $request->getURIData('queryKey'); $controller = id(new PhabricatorApplicationSearchController()) ->setQueryKey($querykey) - ->setSearchEngine(new DrydockResourceSearchEngine()) + ->setSearchEngine($engine) ->setNavigation($this->buildSideNavView()); return $this->delegateToController($controller); 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 4cc7349dac..0641ced96d 100644 --- a/src/applications/drydock/controller/DrydockResourceViewController.php +++ b/src/applications/drydock/controller/DrydockResourceViewController.php @@ -17,6 +17,8 @@ final class DrydockResourceViewController extends DrydockResourceController { $title = pht('Resource %s %s', $resource->getID(), $resource->getName()); $header = id(new PHUIHeaderView()) + ->setUser($viewer) + ->setPolicyObject($resource) ->setHeader($title); $actions = $this->buildActionListView($resource); @@ -25,17 +27,6 @@ final class DrydockResourceViewController extends DrydockResourceController { $resource_uri = 'resource/'.$resource->getID().'/'; $resource_uri = $this->getApplicationURI($resource_uri); - $leases = id(new DrydockLeaseQuery()) - ->setViewer($viewer) - ->withResourceIDs(array($resource->getID())) - ->execute(); - - $lease_list = id(new DrydockLeaseListView()) - ->setUser($viewer) - ->setLeases($leases) - ->render(); - $lease_list->setNoDataString(pht('This resource has no leases.')); - $pager = new PHUIPagerView(); $pager->setURI(new PhutilURI($resource_uri), 'offset'); $pager->setOffset($request->getInt('offset')); @@ -54,16 +45,27 @@ final class DrydockResourceViewController extends DrydockResourceController { $crumbs = $this->buildApplicationCrumbs(); $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); + ->addPropertyList($properties, pht('Properties')) + ->addPropertyList($locks, pht('Slot Locks')) + ->addPropertyList($commands, pht('Commands')); + + $lease_box = $this->buildLeaseBox($resource); + + $log_box = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Resource Logs')) + ->setTable($log_table); return $this->buildApplicationPage( array( $crumbs, $object_box, - $lease_list, - $log_table, + $lease_box, + $log_box, ), array( 'title' => $title, @@ -72,22 +74,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; } @@ -95,9 +104,10 @@ final class DrydockResourceViewController extends DrydockResourceController { private function buildPropertyListView( DrydockResource $resource, PhabricatorActionListView $actions) { + $viewer = $this->getViewer(); - $view = new PHUIPropertyListView(); - $view->setActionList($actions); + $view = id(new PHUIPropertyListView()) + ->setActionList($actions); $status = $resource->getStatus(); $status = DrydockResourceStatus::getNameForStatus($status); @@ -110,14 +120,14 @@ final class DrydockResourceViewController extends DrydockResourceController { pht('Resource Type'), $resource->getType()); - // TODO: Load handle. $view->addProperty( pht('Blueprint'), - $resource->getBlueprintPHID()); + $viewer->renderHandle($resource->getBlueprintPHID())); $attributes = $resource->getAttributes(); if ($attributes) { - $view->addSectionHeader(pht('Attributes')); + $view->addSectionHeader( + pht('Attributes'), 'fa-list-ul'); foreach ($attributes as $key => $value) { $view->addProperty($key, $value); } @@ -126,4 +136,43 @@ final class DrydockResourceViewController extends DrydockResourceController { return $view; } + private function buildLeaseBox(DrydockResource $resource) { + $viewer = $this->getViewer(); + + $leases = id(new DrydockLeaseQuery()) + ->setViewer($viewer) + ->withResourcePHIDs(array($resource->getPHID())) + ->withStatuses( + array( + DrydockLeaseStatus::STATUS_PENDING, + DrydockLeaseStatus::STATUS_ACQUIRED, + DrydockLeaseStatus::STATUS_ACTIVE, + )) + ->setLimit(100) + ->execute(); + + $id = $resource->getID(); + $leases_uri = "resource/{$id}/leases/query/all/"; + $leases_uri = $this->getApplicationURI($leases_uri); + + $lease_header = id(new PHUIHeaderView()) + ->setHeader(pht('Active Leases')) + ->addActionLink( + id(new PHUIButtonView()) + ->setTag('a') + ->setHref($leases_uri) + ->setIconFont('fa-search') + ->setText(pht('View All Leases'))); + + $lease_list = id(new DrydockLeaseListView()) + ->setUser($viewer) + ->setLeases($leases) + ->render() + ->setNoDataString(pht('This resource has no active leases.')); + + return id(new PHUIObjectBoxView()) + ->setHeader($lease_header) + ->setObjectList($lease_list); + } + } diff --git a/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php b/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php index 3f4a2acc67..f4e6ba3a27 100644 --- a/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php +++ b/src/applications/drydock/customfield/DrydockBlueprintCoreCustomField.php @@ -9,6 +9,12 @@ final class DrydockBlueprintCoreCustomField } public function createFields($object) { + // If this is a generic object without an attached implementation (for + // example, via ApplicationSearch), just don't build any custom fields. + if (!$object->hasImplementation()) { + return array(); + } + $impl = $object->getImplementation(); $specs = $impl->getFieldSpecifications(); @@ -40,4 +46,8 @@ final class DrydockBlueprintCoreCustomField return; } + public function getBlueprintFieldValue() { + return $this->getProxy()->getFieldValue(); + } + } diff --git a/src/applications/drydock/customfield/DrydockBlueprintCustomField.php b/src/applications/drydock/customfield/DrydockBlueprintCustomField.php index 37d27ccdd6..700131e753 100644 --- a/src/applications/drydock/customfield/DrydockBlueprintCustomField.php +++ b/src/applications/drydock/customfield/DrydockBlueprintCustomField.php @@ -1,4 +1,8 @@ getTransactionType()) { case DrydockBlueprintTransaction::TYPE_NAME: return $object->getBlueprintName(); + case DrydockBlueprintTransaction::TYPE_DISABLED: + return (int)$object->getIsDisabled(); } + + return parent::getCustomTransactionOldValue($object, $xaction); } protected function getCustomTransactionNewValue( @@ -38,7 +44,11 @@ final class DrydockBlueprintEditor switch ($xaction->getTransactionType()) { case DrydockBlueprintTransaction::TYPE_NAME: return $xaction->getNewValue(); + case DrydockBlueprintTransaction::TYPE_DISABLED: + return (int)$xaction->getNewValue(); } + + return parent::getCustomTransactionNewValue($object, $xaction); } protected function applyCustomInternalTransaction( @@ -48,26 +58,26 @@ final class DrydockBlueprintEditor switch ($xaction->getTransactionType()) { case DrydockBlueprintTransaction::TYPE_NAME: $object->setBlueprintName($xaction->getNewValue()); - break; + return; + case DrydockBlueprintTransaction::TYPE_DISABLED: + $object->setIsDisabled((int)$xaction->getNewValue()); + return; } + + return parent::applyCustomInternalTransaction($object, $xaction); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { - return; - } - protected function extractFilePHIDsFromCustomTransaction( - PhabricatorLiskDAO $object, - PhabricatorApplicationTransaction $xaction) { - return array(); - } + switch ($xaction->getTransactionType()) { + case DrydockBlueprintTransaction::TYPE_NAME: + case DrydockBlueprintTransaction::TYPE_DISABLED: + return; + } - protected function shouldSendMail( - PhabricatorLiskDAO $object, - array $xactions) { - return false; + return parent::applyCustomExternalTransaction($object, $xaction); } } diff --git a/src/applications/drydock/exception/DrydockSlotLockException.php b/src/applications/drydock/exception/DrydockSlotLockException.php new file mode 100644 index 0000000000..2bcf5f8cd7 --- /dev/null +++ b/src/applications/drydock/exception/DrydockSlotLockException.php @@ -0,0 +1,25 @@ +lockMap = $locks; + + if ($locks) { + $lock_list = array(); + foreach ($locks as $lock => $owner_phid) { + $lock_list[] = pht('"%s" (owned by "%s")', $lock, $owner_phid); + } + $message = pht( + 'Unable to acquire slot locks: %s.', + implode(', ', $lock_list)); + } else { + $message = pht('Unable to acquire slot locks.'); + } + + parent::__construct($message); + } + +} diff --git a/src/applications/drydock/interface/DrydockInterface.php b/src/applications/drydock/interface/DrydockInterface.php index 631481f7eb..3b4863e1f4 100644 --- a/src/applications/drydock/interface/DrydockInterface.php +++ b/src/applications/drydock/interface/DrydockInterface.php @@ -2,12 +2,12 @@ abstract class DrydockInterface extends Phobject { - private $config; + private $config = array(); abstract public function getInterfaceType(); - final public function setConfiguration(array $config) { - $this->config = $config; + final public function setConfig($key, $value) { + $this->config[$key] = $value; return $this; } diff --git a/src/applications/drydock/interface/command/DrydockCommandInterface.php b/src/applications/drydock/interface/command/DrydockCommandInterface.php index e2239c1045..8034e0f732 100644 --- a/src/applications/drydock/interface/command/DrydockCommandInterface.php +++ b/src/applications/drydock/interface/command/DrydockCommandInterface.php @@ -2,6 +2,8 @@ abstract class DrydockCommandInterface extends DrydockInterface { + const INTERFACE_TYPE = 'command'; + private $workingDirectory; public function setWorkingDirectory($working_directory) { @@ -14,7 +16,7 @@ abstract class DrydockCommandInterface extends DrydockInterface { } final public function getInterfaceType() { - return 'command'; + return self::INTERFACE_TYPE; } final public function exec($command) { @@ -38,7 +40,7 @@ abstract class DrydockCommandInterface extends DrydockInterface { protected function applyWorkingDirectoryToArgv(array $argv) { if ($this->getWorkingDirectory() !== null) { $cmd = $argv[0]; - $cmd = "(cd %s; {$cmd})"; + $cmd = "(cd %s && {$cmd})"; $argv = array_merge( array($cmd), array($this->getWorkingDirectory()), diff --git a/src/applications/drydock/interface/command/DrydockLocalCommandInterface.php b/src/applications/drydock/interface/command/DrydockLocalCommandInterface.php deleted file mode 100644 index e791214733..0000000000 --- a/src/applications/drydock/interface/command/DrydockLocalCommandInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -applyWorkingDirectoryToArgv($argv); - - return newv('ExecFuture', $argv); - } - -} diff --git a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php index d75dcfb780..1aab14b57b 100644 --- a/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php +++ b/src/applications/drydock/interface/command/DrydockSSHCommandInterface.php @@ -2,35 +2,19 @@ final class DrydockSSHCommandInterface extends DrydockCommandInterface { - private $passphraseSSHKey; + private $credential; private $connectTimeout; - private function openCredentialsIfNotOpen() { - if ($this->passphraseSSHKey !== null) { - return; + private function loadCredential() { + if ($this->credential === null) { + $credential_phid = $this->getConfig('credentialPHID'); + + $this->credential = PassphraseSSHKey::loadFromPHID( + $credential_phid, + PhabricatorUser::getOmnipotentUser()); } - $credential = id(new PassphraseCredentialQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->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()); + return $this->credential; } public function setConnectTimeout($timeout) { @@ -39,30 +23,36 @@ final class DrydockSSHCommandInterface extends DrydockCommandInterface { } public function getExecFuture($command) { - $this->openCredentialsIfNotOpen(); + $credential = $this->loadCredential(); $argv = func_get_args(); $argv = $this->applyWorkingDirectoryToArgv($argv); $full_command = call_user_func_array('csprintf', $argv); - $command_timeout = ''; - if ($this->connectTimeout !== null) { - $command_timeout = csprintf( - '-o %s', - 'ConnectTimeout='.$this->connectTimeout); + $flags = array(); + $flags[] = '-o'; + $flags[] = 'LogLevel=quiet'; + + $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( - 'ssh '. - '-o LogLevel=quiet '. - '-o StrictHostKeyChecking=no '. - '-o UserKnownHostsFile=/dev/null '. - '-o BatchMode=yes '. - '%C -p %s -i %P %P@%s -- %s', - $command_timeout, + 'ssh %Ls -l %P -p %s -i %P %s -- %s', + $flags, + $credential->getUsernameEnvelope(), $this->getConfig('port'), - $this->passphraseSSHKey->getKeyfileEnvelope(), - $this->passphraseSSHKey->getUsernameEnvelope(), + $credential->getKeyfileEnvelope(), $this->getConfig('host'), $full_command); } 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/DrydockManagementCommandWorkflow.php b/src/applications/drydock/management/DrydockManagementCommandWorkflow.php new file mode 100644 index 0000000000..bc66966f8c --- /dev/null +++ b/src/applications/drydock/management/DrydockManagementCommandWorkflow.php @@ -0,0 +1,66 @@ +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; + } + +} diff --git a/src/applications/drydock/management/DrydockManagementCreateResourceWorkflow.php b/src/applications/drydock/management/DrydockManagementCreateResourceWorkflow.php deleted file mode 100644 index 8c67aaa908..0000000000 --- a/src/applications/drydock/management/DrydockManagementCreateResourceWorkflow.php +++ /dev/null @@ -1,81 +0,0 @@ -setName('create-resource') - ->setSynopsis(pht('Create a resource manually.')) - ->setArguments( - array( - array( - 'name' => 'name', - 'param' => 'resource_name', - 'help' => pht('Resource name.'), - ), - array( - 'name' => 'blueprint', - 'param' => 'blueprint_id', - 'help' => pht('Blueprint ID.'), - ), - array( - 'name' => 'attributes', - 'param' => 'name=value,...', - 'help' => pht('Resource attributes.'), - ), - )); - } - - public function execute(PhutilArgumentParser $args) { - $console = PhutilConsole::getConsole(); - - $resource_name = $args->getArg('name'); - if (!$resource_name) { - throw new PhutilArgumentUsageException( - pht( - 'Specify a resource name with `%s`.', - '--name')); - } - - $blueprint_id = $args->getArg('blueprint'); - if (!$blueprint_id) { - throw new PhutilArgumentUsageException( - pht( - 'Specify a blueprint ID with `%s`.', - '--blueprint')); - } - - $attributes = $args->getArg('attributes'); - if ($attributes) { - $options = new PhutilSimpleOptions(); - $options->setCaseSensitive(true); - $attributes = $options->parse($attributes); - } - - $viewer = $this->getViewer(); - - $blueprint = id(new DrydockBlueprintQuery()) - ->setViewer($viewer) - ->withIDs(array($blueprint_id)) - ->executeOne(); - if (!$blueprint) { - throw new PhutilArgumentUsageException( - pht('Specified blueprint does not exist.')); - } - - $resource = id(new DrydockResource()) - ->setBlueprintPHID($blueprint->getPHID()) - ->setType($blueprint->getImplementation()->getType()) - ->setName($resource_name) - ->setStatus(DrydockResourceStatus::STATUS_OPEN); - if ($attributes) { - $resource->setAttributes($attributes); - } - $resource->save(); - - $console->writeOut("%s\n", pht('Created Resource %s', $resource->getID())); - return 0; - } - -} diff --git a/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php b/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php index 238177ae62..4c1e0d875c 100644 --- a/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php +++ b/src/applications/drydock/management/DrydockManagementLeaseWorkflow.php @@ -14,6 +14,11 @@ final class DrydockManagementLeaseWorkflow 'param' => 'resource_type', 'help' => pht('Resource type.'), ), + array( + 'name' => 'until', + 'param' => 'time', + 'help' => pht('Set lease expiration time.'), + ), array( 'name' => 'attributes', 'param' => 'name=value,...', @@ -33,6 +38,17 @@ final class DrydockManagementLeaseWorkflow '--type')); } + $until = $args->getArg('until'); + if (strlen($until)) { + $until = strtotime($until); + if ($until <= 0) { + throw new PhutilArgumentUsageException( + pht( + 'Unable to parse argument to "%s".', + '--until')); + } + } + $attributes = $args->getArg('attributes'); if ($attributes) { $options = new PhutilSimpleOptions(); @@ -40,18 +56,29 @@ final class DrydockManagementLeaseWorkflow $attributes = $options->parse($attributes); } - PhabricatorWorker::setRunAllTasksInProcess(true); - $lease = id(new DrydockLease()) ->setResourceType($resource_type); + if ($attributes) { $lease->setAttributes($attributes); } - $lease - ->queueForActivation() - ->waitUntilActive(); - $console->writeOut("%s\n", pht('Acquired Lease %s', $lease->getID())); + if ($until) { + $lease->setUntil($until); + } + + $lease->queueForActivation(); + + echo tsprintf( + "%s\n", + pht('Waiting for daemons to activate lease...')); + + $lease->waitUntilActive(); + + echo tsprintf( + "%s\n", + pht('Activated lease "%s".', $lease->getID())); + return 0; } 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/phid/DrydockBlueprintPHIDType.php b/src/applications/drydock/phid/DrydockBlueprintPHIDType.php index b61eb396b9..86eeb7f3c5 100644 --- a/src/applications/drydock/phid/DrydockBlueprintPHIDType.php +++ b/src/applications/drydock/phid/DrydockBlueprintPHIDType.php @@ -29,6 +29,7 @@ final class DrydockBlueprintPHIDType extends PhabricatorPHIDType { $blueprint = $objects[$phid]; $id = $blueprint->getID(); + $handle->setName($blueprint->getBlueprintName()); $handle->setURI("/drydock/blueprint/{$id}/"); } } diff --git a/src/applications/drydock/phid/DrydockResourcePHIDType.php b/src/applications/drydock/phid/DrydockResourcePHIDType.php index 1bfcc68597..6b266ff169 100644 --- a/src/applications/drydock/phid/DrydockResourcePHIDType.php +++ b/src/applications/drydock/phid/DrydockResourcePHIDType.php @@ -29,7 +29,12 @@ final class DrydockResourcePHIDType extends PhabricatorPHIDType { $resource = $objects[$phid]; $id = $resource->getID(); - $handle->setName($resource->getName()); + $handle->setName( + pht( + 'Resource %d: %s', + $id, + $resource->getName())); + $handle->setURI("/drydock/resource/{$id}/"); } } diff --git a/src/applications/drydock/query/DrydockBlueprintQuery.php b/src/applications/drydock/query/DrydockBlueprintQuery.php index abad714d8f..7ce5dcbe5b 100644 --- a/src/applications/drydock/query/DrydockBlueprintQuery.php +++ b/src/applications/drydock/query/DrydockBlueprintQuery.php @@ -4,7 +4,9 @@ final class DrydockBlueprintQuery extends DrydockQuery { private $ids; private $phids; + private $blueprintClasses; private $datasourceQuery; + private $disabled; public function withIDs(array $ids) { $this->ids = $ids; @@ -16,63 +18,84 @@ final class DrydockBlueprintQuery extends DrydockQuery { return $this; } + public function withBlueprintClasses(array $classes) { + $this->blueprintClasses = $classes; + return $this; + } + public function withDatasourceQuery($query) { $this->datasourceQuery = $query; return $this; } + public function withDisabled($disabled) { + $this->disabled = $disabled; + return $this; + } + + public function newResultObject() { + return new DrydockBlueprint(); + } + protected function loadPage() { - $table = new DrydockBlueprint(); - $conn_r = $table->establishConnection('r'); + return $this->loadStandardPage($this->newResultObject()); + } - $data = queryfx_all( - $conn_r, - 'SELECT blueprint.* FROM %T blueprint %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); - - $blueprints = $table->loadAllFromArray($data); - - $implementations = - DrydockBlueprintImplementation::getAllBlueprintImplementations(); - - foreach ($blueprints as $blueprint) { - if (array_key_exists($blueprint->getClassName(), $implementations)) { - $blueprint->attachImplementation( - $implementations[$blueprint->getClassName()]); + protected function willFilterPage(array $blueprints) { + $impls = DrydockBlueprintImplementation::getAllBlueprintImplementations(); + foreach ($blueprints as $key => $blueprint) { + $impl = idx($impls, $blueprint->getClassName()); + if (!$impl) { + $this->didRejectResult($blueprint); + unset($blueprints[$key]); + continue; } + $impl = clone $impl; + $blueprint->attachImplementation($impl); } return $blueprints; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } if ($this->datasourceQuery !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'blueprintName LIKE %>', $this->datasourceQuery); } - return $this->formatWhereClause($where); + if ($this->blueprintClasses !== null) { + $where[] = qsprintf( + $conn, + 'className IN (%Ls)', + $this->blueprintClasses); + } + + if ($this->disabled !== null) { + $where[] = qsprintf( + $conn, + 'isDisabled = %d', + (int)$this->disabled); + } + + return $where; } } diff --git a/src/applications/drydock/query/DrydockBlueprintSearchEngine.php b/src/applications/drydock/query/DrydockBlueprintSearchEngine.php index ccb4c80803..64859649df 100644 --- a/src/applications/drydock/query/DrydockBlueprintSearchEngine.php +++ b/src/applications/drydock/query/DrydockBlueprintSearchEngine.php @@ -11,17 +11,31 @@ final class DrydockBlueprintSearchEngine return 'PhabricatorDrydockApplication'; } - public function buildSavedQueryFromRequest(AphrontRequest $request) { - return new PhabricatorSavedQuery(); + public function newQuery() { + return id(new DrydockBlueprintQuery()); } - public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { - return new DrydockBlueprintQuery(); + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); + + if ($map['isDisabled'] !== null) { + $query->withDisabled($map['isDisabled']); + } + + return $query; } - public function buildSearchForm( - AphrontFormView $form, - PhabricatorSavedQuery $saved) {} + protected function buildCustomSearchFields() { + return array( + id(new PhabricatorSearchThreeStateField()) + ->setLabel(pht('Disabled')) + ->setKey('isDisabled') + ->setOptions( + pht('(Show All)'), + pht('Show Only Disabled Blueprints'), + pht('Hide Disabled Blueprints')), + ); + } protected function getURI($path) { return '/drydock/blueprint/'.$path; @@ -29,6 +43,7 @@ final class DrydockBlueprintSearchEngine protected function getBuiltinQueryNames() { return array( + 'active' => pht('Active Blueprints'), 'all' => pht('All Blueprints'), ); } @@ -38,6 +53,8 @@ final class DrydockBlueprintSearchEngine $query->setQueryKey($query_key); switch ($query_key) { + case 'active': + return $query->setParameter('isDisabled', false); case 'all': return $query; } @@ -62,6 +79,12 @@ final class DrydockBlueprintSearchEngine if (!$blueprint->getImplementation()->isEnabled()) { $item->setDisabled(true); + $item->addIcon('fa-chain-broken grey', pht('Implementation')); + } + + if ($blueprint->getIsDisabled()) { + $item->setDisabled(true); + $item->addIcon('fa-ban grey', pht('Disabled')); } $item->addAttribute($blueprint->getImplementation()->getBlueprintName()); 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 37f02bd748..c7c770bbea 100644 --- a/src/applications/drydock/query/DrydockLeaseQuery.php +++ b/src/applications/drydock/query/DrydockLeaseQuery.php @@ -4,9 +4,10 @@ final class DrydockLeaseQuery extends DrydockQuery { private $ids; private $phids; - private $resourceIDs; + private $resourcePHIDs; private $statuses; private $datasourceQuery; + private $needCommands; public function withIDs(array $ids) { $this->ids = $ids; @@ -18,8 +19,8 @@ final class DrydockLeaseQuery extends DrydockQuery { return $this; } - public function withResourceIDs(array $ids) { - $this->resourceIDs = $ids; + public function withResourcePHIDs(array $phids) { + $this->resourcePHIDs = $phids; return $this; } @@ -28,36 +29,38 @@ final class DrydockLeaseQuery extends DrydockQuery { return $this; } - public function newResultObject() { - return new DrydockLease(); - } - public function withDatasourceQuery($query) { $this->datasourceQuery = $query; return $this; } + public function newResultObject() { + return new DrydockLease(); + } + protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } protected function willFilterPage(array $leases) { - $resource_ids = array_filter(mpull($leases, 'getResourceID')); - if ($resource_ids) { + $resource_phids = array_filter(mpull($leases, 'getResourcePHID')); + if ($resource_phids) { $resources = id(new DrydockResourceQuery()) ->setParentQuery($this) ->setViewer($this->getViewer()) - ->withIDs(array_unique($resource_ids)) + ->withPHIDs(array_unique($resource_phids)) ->execute(); + $resources = mpull($resources, null, 'getPHID'); } else { $resources = array(); } foreach ($leases as $key => $lease) { $resource = null; - if ($lease->getResourceID()) { - $resource = idx($resources, $lease->getResourceID()); + if ($lease->getResourcePHID()) { + $resource = idx($resources, $lease->getResourcePHID()); if (!$resource) { + $this->didRejectResult($lease); unset($leases[$key]); continue; } @@ -71,11 +74,11 @@ final class DrydockLeaseQuery extends DrydockQuery { protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); - if ($this->resourceIDs !== null) { + if ($this->resourcePHIDs !== null) { $where[] = qsprintf( $conn, - 'resourceID IN (%Ld)', - $this->resourceIDs); + 'resourcePHID IN (%Ls)', + $this->resourcePHIDs); } if ($this->ids !== null) { @@ -95,7 +98,7 @@ final class DrydockLeaseQuery extends DrydockQuery { if ($this->statuses !== null) { $where[] = qsprintf( $conn, - 'status IN (%Ld)', + 'status IN (%Ls)', $this->statuses); } diff --git a/src/applications/drydock/query/DrydockLeaseSearchEngine.php b/src/applications/drydock/query/DrydockLeaseSearchEngine.php index 66f783875e..3b551023b2 100644 --- a/src/applications/drydock/query/DrydockLeaseSearchEngine.php +++ b/src/applications/drydock/query/DrydockLeaseSearchEngine.php @@ -3,6 +3,17 @@ final class DrydockLeaseSearchEngine extends PhabricatorApplicationSearchEngine { + private $resource; + + public function setResource($resource) { + $this->resource = $resource; + return $this; + } + + public function getResource() { + return $this->resource; + } + public function getResultTypeDescription() { return pht('Drydock Leases'); } @@ -11,50 +22,44 @@ final class DrydockLeaseSearchEngine return 'PhabricatorDrydockApplication'; } - public function buildSavedQueryFromRequest(AphrontRequest $request) { - $saved = new PhabricatorSavedQuery(); + public function newQuery() { + $query = new DrydockLeaseQuery(); - $saved->setParameter( - 'statuses', - $this->readListFromRequest($request, 'statuses')); - - return $saved; - } - - public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { - $query = id(new DrydockLeaseQuery()); - - $statuses = $saved->getParameter('statuses', array()); - if ($statuses) { - $query->withStatuses($statuses); + $resource = $this->getResource(); + if ($resource) { + $query->withResourcePHIDs(array($resource->getPHID())); } return $query; } - public function buildSearchForm( - AphrontFormView $form, - PhabricatorSavedQuery $saved) { + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); - $statuses = $saved->getParameter('statuses', array()); - - $status_control = id(new AphrontFormCheckboxControl()) - ->setLabel(pht('Status')); - foreach (DrydockLeaseStatus::getAllStatuses() as $status) { - $status_control->addCheckbox( - 'statuses[]', - $status, - DrydockLeaseStatus::getNameForStatus($status), - in_array($status, $statuses)); + if ($map['statuses']) { + $query->withStatuses($map['statuses']); } - $form - ->appendChild($status_control); + return $query; + } + protected function buildCustomSearchFields() { + return array( + id(new PhabricatorSearchCheckboxesField()) + ->setLabel(pht('Statuses')) + ->setKey('statuses') + ->setOptions(DrydockLeaseStatus::getStatusMap()), + ); } protected function getURI($path) { - return '/drydock/lease/'.$path; + $resource = $this->getResource(); + if ($resource) { + $id = $resource->getID(); + return "/drydock/resource/{$id}/leases/".$path; + } else { + return '/drydock/lease/'.$path; + } } protected function getBuiltinQueryNames() { @@ -74,7 +79,7 @@ final class DrydockLeaseSearchEngine 'statuses', array( DrydockLeaseStatus::STATUS_PENDING, - DrydockLeaseStatus::STATUS_ACQUIRING, + DrydockLeaseStatus::STATUS_ACQUIRED, DrydockLeaseStatus::STATUS_ACTIVE, )); case 'all': diff --git a/src/applications/drydock/query/DrydockResourceQuery.php b/src/applications/drydock/query/DrydockResourceQuery.php index d07e729276..d15b737141 100644 --- a/src/applications/drydock/query/DrydockResourceQuery.php +++ b/src/applications/drydock/query/DrydockResourceQuery.php @@ -39,71 +39,82 @@ final class DrydockResourceQuery extends DrydockQuery { return $this; } + public function newResultObject() { + return new DrydockResource(); + } + protected function loadPage() { - $table = new DrydockResource(); - $conn_r = $table->establishConnection('r'); + return $this->loadStandardPage($this->newResultObject()); + } - $data = queryfx_all( - $conn_r, - 'SELECT resource.* FROM %T resource %Q %Q %Q', - $table->getTableName(), - $this->buildWhereClause($conn_r), - $this->buildOrderClause($conn_r), - $this->buildLimitClause($conn_r)); + protected function willFilterPage(array $resources) { + $blueprint_phids = mpull($resources, 'getBlueprintPHID'); - $resources = $table->loadAllFromArray($data); + $blueprints = id(new DrydockBlueprintQuery()) + ->setViewer($this->getViewer()) + ->withPHIDs($blueprint_phids) + ->execute(); + $blueprints = mpull($blueprints, null, 'getPHID'); + + foreach ($resources as $key => $resource) { + $blueprint = idx($blueprints, $resource->getBlueprintPHID()); + if (!$blueprint) { + $this->didRejectResult($resource); + unset($resources[$key]); + continue; + } + $resource->attachBlueprint($blueprint); + } return $resources; } - protected function buildWhereClause(AphrontDatabaseConnection $conn_r) { - $where = array(); + protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { + $where = parent::buildWhereClauseParts($conn); if ($this->ids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'id IN (%Ld)', $this->ids); } if ($this->phids !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'phid IN (%Ls)', $this->phids); } if ($this->types !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'type IN (%Ls)', $this->types); } if ($this->statuses !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'status IN (%Ls)', $this->statuses); } if ($this->blueprintPHIDs !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'blueprintPHID IN (%Ls)', $this->blueprintPHIDs); } if ($this->datasourceQuery !== null) { $where[] = qsprintf( - $conn_r, + $conn, 'name LIKE %>', $this->datasourceQuery); } - $where[] = $this->buildPagingClause($conn_r); - - return $this->formatWhereClause($where); + return $where; } } diff --git a/src/applications/drydock/query/DrydockResourceSearchEngine.php b/src/applications/drydock/query/DrydockResourceSearchEngine.php index 5a47e40721..8f72fdf217 100644 --- a/src/applications/drydock/query/DrydockResourceSearchEngine.php +++ b/src/applications/drydock/query/DrydockResourceSearchEngine.php @@ -3,6 +3,17 @@ final class DrydockResourceSearchEngine extends PhabricatorApplicationSearchEngine { + private $blueprint; + + public function setBlueprint(DrydockBlueprint $blueprint) { + $this->blueprint = $blueprint; + return $this; + } + + public function getBlueprint() { + return $this->blueprint; + } + public function getResultTypeDescription() { return pht('Drydock Resources'); } @@ -11,49 +22,44 @@ final class DrydockResourceSearchEngine return 'PhabricatorDrydockApplication'; } - public function buildSavedQueryFromRequest(AphrontRequest $request) { - $saved = new PhabricatorSavedQuery(); + public function newQuery() { + $query = new DrydockResourceQuery(); - $saved->setParameter( - 'statuses', - $this->readListFromRequest($request, 'statuses')); - - return $saved; - } - - public function buildQueryFromSavedQuery(PhabricatorSavedQuery $saved) { - $query = id(new DrydockResourceQuery()); - - $statuses = $saved->getParameter('statuses', array()); - if ($statuses) { - $query->withStatuses($statuses); + $blueprint = $this->getBlueprint(); + if ($blueprint) { + $query->withBlueprintPHIDs(array($blueprint->getPHID())); } return $query; } - public function buildSearchForm( - AphrontFormView $form, - PhabricatorSavedQuery $saved) { + protected function buildQueryFromParameters(array $map) { + $query = $this->newQuery(); - $statuses = $saved->getParameter('statuses', array()); - - $status_control = id(new AphrontFormCheckboxControl()) - ->setLabel(pht('Status')); - foreach (DrydockResourceStatus::getAllStatuses() as $status) { - $status_control->addCheckbox( - 'statuses[]', - $status, - DrydockResourceStatus::getNameForStatus($status), - in_array($status, $statuses)); + if ($map['statuses']) { + $query->withStatuses($map['statuses']); } - $form - ->appendChild($status_control); + return $query; + } + + protected function buildCustomSearchFields() { + return array( + id(new PhabricatorSearchCheckboxesField()) + ->setLabel(pht('Statuses')) + ->setKey('statuses') + ->setOptions(DrydockResourceStatus::getStatusMap()), + ); } protected function getURI($path) { - return '/drydock/resource/'.$path; + $blueprint = $this->getBlueprint(); + if ($blueprint) { + $id = $blueprint->getID(); + return "/drydock/blueprint/{$id}/resources/".$path; + } else { + return '/drydock/resource/'.$path; + } } protected function getBuiltinQueryNames() { @@ -73,7 +79,7 @@ final class DrydockResourceSearchEngine 'statuses', array( DrydockResourceStatus::STATUS_PENDING, - DrydockResourceStatus::STATUS_OPEN, + DrydockResourceStatus::STATUS_ACTIVE, )); case 'all': return $query; diff --git a/src/applications/drydock/storage/DrydockBlueprint.php b/src/applications/drydock/storage/DrydockBlueprint.php index 8185d35388..429e5c2971 100644 --- a/src/applications/drydock/storage/DrydockBlueprint.php +++ b/src/applications/drydock/storage/DrydockBlueprint.php @@ -1,5 +1,9 @@ setViewPolicy($view_policy) ->setEditPolicy($edit_policy) - ->setBlueprintName(''); + ->setBlueprintName('') + ->setIsDisabled(0); } protected function getConfiguration() { @@ -41,6 +48,7 @@ final class DrydockBlueprint extends DrydockDAO self::CONFIG_COLUMN_SCHEMA => array( 'className' => 'text255', 'blueprintName' => 'sort255', + 'isDisabled' => 'bool', ), ) + parent::getConfiguration(); } @@ -51,16 +59,7 @@ final class DrydockBlueprint extends DrydockDAO } public function getImplementation() { - $class = $this->className; - $implementations = - DrydockBlueprintImplementation::getAllBlueprintImplementations(); - if (!isset($implementations[$class])) { - throw new Exception( - pht( - "Invalid class name for blueprint (got '%s')", - $class)); - } - return id(new $class())->attachInstance($this); + return $this->assertAttached($this->implementation); } public function attachImplementation(DrydockBlueprintImplementation $impl) { @@ -68,6 +67,10 @@ final class DrydockBlueprint extends DrydockDAO return $this; } + public function hasImplementation() { + return ($this->implementation !== self::ATTACHABLE); + } + public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } @@ -77,6 +80,175 @@ final class DrydockBlueprint extends DrydockDAO return $this; } + public function getFieldValue($key) { + $key = "std:drydock:core:{$key}"; + $fields = $this->loadCustomFields(); + + $field = idx($fields, $key); + if (!$field) { + throw new Exception( + pht( + 'Unknown blueprint field "%s"!', + $key)); + } + + return $field->getBlueprintFieldValue(); + } + + private function loadCustomFields() { + if ($this->fields === null) { + $field_list = PhabricatorCustomField::getObjectFields( + $this, + PhabricatorCustomField::ROLE_VIEW); + $field_list->readFieldsFromStorage($this); + + $this->fields = $field_list->getFields(); + } + return $this->fields; + } + + +/* -( Allocating Resources )----------------------------------------------- */ + + + /** + * @task resource + */ + public function canEverAllocateResourceForLease(DrydockLease $lease) { + return $this->getImplementation()->canEverAllocateResourceForLease( + $this, + $lease); + } + + + /** + * @task resource + */ + public function canAllocateResourceForLease(DrydockLease $lease) { + return $this->getImplementation()->canAllocateResourceForLease( + $this, + $lease); + } + + + /** + * @task resource + */ + public function allocateResource(DrydockLease $lease) { + return $this->getImplementation()->allocateResource( + $this, + $lease); + } + + + /** + * @task resource + */ + public function activateResource(DrydockResource $resource) { + return $this->getImplementation()->activateResource( + $this, + $resource); + } + + + /** + * @task resource + */ + public function destroyResource(DrydockResource $resource) { + $this->getImplementation()->destroyResource( + $this, + $resource); + return $this; + } + + +/* -( Acquiring Leases )--------------------------------------------------- */ + + + /** + * @task lease + */ + public function canAcquireLeaseOnResource( + DrydockResource $resource, + DrydockLease $lease) { + return $this->getImplementation()->canAcquireLeaseOnResource( + $this, + $resource, + $lease); + } + + + /** + * @task lease + */ + public function acquireLease( + DrydockResource $resource, + DrydockLease $lease) { + return $this->getImplementation()->acquireLease( + $this, + $resource, + $lease); + } + + + /** + * @task lease + */ + public function activateLease( + DrydockResource $resource, + DrydockLease $lease) { + return $this->getImplementation()->activateLease( + $this, + $resource, + $lease); + } + + + /** + * @task lease + */ + public function didReleaseLease( + DrydockResource $resource, + DrydockLease $lease) { + $this->getImplementation()->didReleaseLease( + $this, + $resource, + $lease); + return $this; + } + + + /** + * @task lease + */ + public function destroyLease( + DrydockResource $resource, + DrydockLease $lease) { + $this->getImplementation()->destroyLease( + $this, + $resource, + $lease); + 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 )------------------------- */ diff --git a/src/applications/drydock/storage/DrydockBlueprintTransaction.php b/src/applications/drydock/storage/DrydockBlueprintTransaction.php index 0e3b2e2e3e..0856f01fdb 100644 --- a/src/applications/drydock/storage/DrydockBlueprintTransaction.php +++ b/src/applications/drydock/storage/DrydockBlueprintTransaction.php @@ -3,7 +3,8 @@ final class DrydockBlueprintTransaction extends PhabricatorApplicationTransaction { - const TYPE_NAME = 'drydock:blueprint:name'; + const TYPE_NAME = 'drydock:blueprint:name'; + const TYPE_DISABLED = 'drydock:blueprint:disabled'; public function getApplicationName() { return 'drydock'; @@ -31,6 +32,16 @@ final class DrydockBlueprintTransaction $old, $new); } + case self::TYPE_DISABLED: + if ($new) { + return pht( + '%s disabled this blueprint.', + $author_handle); + } else { + return pht( + '%s enabled this blueprint.', + $author_handle); + } } return parent::getTitle(); 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 39fa59330c..fe38f54fe0 100644 --- a/src/applications/drydock/storage/DrydockLease.php +++ b/src/applications/drydock/storage/DrydockLease.php @@ -3,16 +3,19 @@ final class DrydockLease extends DrydockDAO implements PhabricatorPolicyInterface { - protected $resourceID; + protected $resourcePHID; protected $resourceType; protected $until; protected $ownerPHID; protected $attributes = array(); protected $status = DrydockLeaseStatus::STATUS_PENDING; - protected $taskID; private $resource = self::ATTACHABLE; private $releaseOnDestruction; + private $isAcquired = false; + private $isActivated = false; + private $activateWhenAcquired = false; + private $slotLocks = array(); /** * Flag this lease to be released when its destructor is called. This is @@ -26,11 +29,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() { @@ -44,18 +60,15 @@ final class DrydockLease extends DrydockDAO 'attributes' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( - 'status' => 'uint32', + 'status' => 'text32', 'until' => 'epoch?', 'resourceType' => 'text128', - 'taskID' => 'id?', 'ownerPHID' => 'phid?', - 'resourceID' => 'id?', + 'resourcePHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( - 'key_phid' => null, - 'phid' => array( - 'columns' => array('phid'), - 'unique' => true, + 'key_resource' => array( + 'columns' => array('resourcePHID', 'status'), ), ), ) + parent::getConfiguration(); @@ -91,114 +104,233 @@ final class DrydockLease extends DrydockDAO return ($this->resource !== null); } - public function loadResource() { - return id(new DrydockResource())->loadOneWhere( - 'id = %d', - $this->getResourceID()); - } - public function queueForActivation() { if ($this->getID()) { throw new Exception( pht('Only new leases may be queued for activation!')); } - $this->setStatus(DrydockLeaseStatus::STATUS_PENDING); - $this->save(); + $this + ->setStatus(DrydockLeaseStatus::STATUS_PENDING) + ->save(); $task = PhabricatorWorker::scheduleTask( 'DrydockAllocatorWorker', - $this->getID()); - - // NOTE: Scheduling the task might execute it in-process, if we're running - // from a CLI script. Reload the lease to make sure we have the most - // up-to-date information. Normally, this has no effect. - $this->reload(); - - $this->setTaskID($task->getID()); - $this->save(); + array( + 'leasePHID' => $this->getPHID(), + ), + array( + 'objectPHID' => $this->getPHID(), + )); return $this; } - public function release() { - $this->assertActive(); - $this->setStatus(DrydockLeaseStatus::STATUS_RELEASED); - $this->save(); - - $this->resource = null; - - return $this; - } - - public function isActive() { - switch ($this->status) { - case DrydockLeaseStatus::STATUS_ACTIVE: - case DrydockLeaseStatus::STATUS_ACQUIRING: + public function isActivating() { + switch ($this->getStatus()) { + case DrydockLeaseStatus::STATUS_PENDING: + case DrydockLeaseStatus::STATUS_ACQUIRED: return true; } + return false; } - private function assertActive() { - if (!$this->isActive()) { - throw new Exception( - pht( - 'Lease is not active! You can not interact with resources through '. - 'an inactive lease.')); - } - } - - public static function waitForLeases(array $leases) { - assert_instances_of($leases, __CLASS__); - - $task_ids = array_filter(mpull($leases, 'getTaskID')); - - PhabricatorWorker::waitForTasks($task_ids); - - $unresolved = $leases; - while (true) { - foreach ($unresolved as $key => $lease) { - $lease->reload(); - switch ($lease->getStatus()) { - case DrydockLeaseStatus::STATUS_ACTIVE: - unset($unresolved[$key]); - break; - case DrydockLeaseStatus::STATUS_RELEASED: - throw new Exception(pht('Lease has already been released!')); - case DrydockLeaseStatus::STATUS_EXPIRED: - throw new Exception(pht('Lease has already expired!')); - case DrydockLeaseStatus::STATUS_BROKEN: - throw new Exception(pht('Lease has been broken!')); - case DrydockLeaseStatus::STATUS_PENDING: - case DrydockLeaseStatus::STATUS_ACQUIRING: - break; - default: - throw new Exception(pht('Unknown status??')); - } - } - - if ($unresolved) { - sleep(1); - } else { - break; - } + public function isActive() { + switch ($this->getStatus()) { + case DrydockLeaseStatus::STATUS_ACTIVE: + return true; } - foreach ($leases as $lease) { - $lease->attachResource($lease->loadResource()); - } + return false; } public function waitUntilActive() { - if (!$this->getID()) { - $this->queueForActivation(); + while (true) { + $lease = $this->reload(); + if (!$lease) { + throw new Exception(pht('Failed to reload lease.')); + } + + $status = $lease->getStatus(); + + switch ($status) { + case DrydockLeaseStatus::STATUS_ACTIVE: + return; + case DrydockLeaseStatus::STATUS_RELEASED: + throw new Exception(pht('Lease has already been released!')); + case DrydockLeaseStatus::STATUS_DESTROYED: + throw new Exception(pht('Lease has already been destroyed!')); + case DrydockLeaseStatus::STATUS_BROKEN: + throw new Exception(pht('Lease has been broken!')); + case DrydockLeaseStatus::STATUS_PENDING: + case DrydockLeaseStatus::STATUS_ACQUIRED: + break; + default: + throw new Exception( + pht( + 'Lease has unknown status "%s".', + $status)); + } + + sleep(1); + } + } + + public function setActivateWhenAcquired($activate) { + $this->activateWhenAcquired = true; + return $this; + } + + public function needSlotLock($key) { + $this->slotLocks[] = $key; + return $this; + } + + public function acquireOnResource(DrydockResource $resource) { + $expect_status = DrydockLeaseStatus::STATUS_PENDING; + $actual_status = $this->getStatus(); + if ($actual_status != $expect_status) { + throw new Exception( + pht( + 'Trying to acquire a lease on a resource which is in the wrong '. + 'state: status must be "%s", actually "%s".', + $expect_status, + $actual_status)); + } + + if ($this->activateWhenAcquired) { + $new_status = DrydockLeaseStatus::STATUS_ACTIVE; + } else { + $new_status = DrydockLeaseStatus::STATUS_ACQUIRED; + } + + if ($new_status == DrydockLeaseStatus::STATUS_ACTIVE) { + if ($resource->getStatus() == DrydockResourceStatus::STATUS_PENDING) { + throw new Exception( + pht( + 'Trying to acquire an active lease on a pending resource. '. + 'You can not immediately activate leases on resources which '. + 'need time to start up.')); + } + } + + $this->openTransaction(); + + $this + ->setResourcePHID($resource->getPHID()) + ->setStatus($new_status) + ->save(); + + DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks); + $this->slotLocks = array(); + + $this->saveTransaction(); + + $this->isAcquired = true; + + if ($new_status == DrydockLeaseStatus::STATUS_ACTIVE) { + $this->didActivate(); } - self::waitForLeases(array($this)); return $this; } + public function isAcquiredLease() { + return $this->isAcquired; + } + + public function activateOnResource(DrydockResource $resource) { + $expect_status = DrydockLeaseStatus::STATUS_ACQUIRED; + $actual_status = $this->getStatus(); + if ($actual_status != $expect_status) { + throw new Exception( + pht( + 'Trying to activate a lease which has the wrong status: status '. + 'must be "%s", actually "%s".', + $expect_status, + $actual_status)); + } + + if ($resource->getStatus() == DrydockResourceStatus::STATUS_PENDING) { + // TODO: Be stricter about this? + throw new Exception( + pht( + 'Trying to activate a lease on a pending resource.')); + } + + $this->openTransaction(); + + $this + ->setStatus(DrydockLeaseStatus::STATUS_ACTIVE) + ->save(); + + DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks); + $this->slotLocks = array(); + + $this->saveTransaction(); + + $this->isActivated = true; + + $this->didActivate(); + + return $this; + } + + public function isActivatedLease() { + return $this->isActivated; + } + + public function canRelease() { + if (!$this->getID()) { + return false; + } + + switch ($this->getStatus()) { + case DrydockLeaseStatus::STATUS_RELEASED: + case DrydockLeaseStatus::STATUS_DESTROYED: + return false; + default: + return true; + } + } + + public function scheduleUpdate($epoch = null) { + PhabricatorWorker::scheduleTask( + 'DrydockLeaseUpdateWorker', + array( + 'leasePHID' => $this->getPHID(), + 'isExpireTask' => ($epoch !== null), + ), + array( + 'objectPHID' => $this->getPHID(), + 'delayUntil' => $epoch, + )); + } + + 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(); + } + + $expires = $this->getUntil(); + if ($expires) { + $this->scheduleUpdate($expires); + } + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */ @@ -206,6 +338,7 @@ final class DrydockLease extends DrydockDAO public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, ); } @@ -213,6 +346,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 d078c76c2f..f2be89a6f2 100644 --- a/src/applications/drydock/storage/DrydockResource.php +++ b/src/applications/drydock/storage/DrydockResource.php @@ -14,7 +14,11 @@ final class DrydockResource extends DrydockDAO protected $capabilities = array(); protected $ownerPHID; - private $blueprint; + private $blueprint = self::ATTACHABLE; + private $isAllocated = false; + private $isActivated = false; + private $activateWhenAllocated = false; + private $slotLocks = array(); protected function getConfiguration() { return array( @@ -26,14 +30,15 @@ final class DrydockResource extends DrydockDAO self::CONFIG_COLUMN_SCHEMA => array( 'name' => 'text255', 'ownerPHID' => 'phid?', - 'status' => 'uint32', + 'status' => 'text32', 'type' => 'text64', ), self::CONFIG_KEY_SCHEMA => array( - 'key_phid' => null, - 'phid' => array( - 'columns' => array('phid'), - 'unique' => true, + 'key_type' => array( + 'columns' => array('type', 'status'), + ), + 'key_blueprint' => array( + 'columns' => array('blueprintPHID', 'status'), ), ), ) + parent::getConfiguration(); @@ -65,46 +70,145 @@ final class DrydockResource extends DrydockDAO } public function getBlueprint() { - // TODO: Policy stuff. - if (empty($this->blueprint)) { - $blueprint = id(new DrydockBlueprint()) - ->loadOneWhere('phid = %s', $this->blueprintPHID); - $this->blueprint = $blueprint->getImplementation(); - } - return $this->blueprint; + return $this->assertAttached($this->blueprint); } - public function closeResource() { + public function attachBlueprint(DrydockBlueprint $blueprint) { + $this->blueprint = $blueprint; + return $this; + } + + public function setActivateWhenAllocated($activate) { + $this->activateWhenAllocated = $activate; + return $this; + } + + public function needSlotLock($key) { + $this->slotLocks[] = $key; + return $this; + } + + public function allocateResource() { + if ($this->getID()) { + throw new Exception( + pht( + 'Trying to allocate a resource which has already been persisted. '. + 'Only new resources may be allocated.')); + } + + $expect_status = DrydockResourceStatus::STATUS_PENDING; + $actual_status = $this->getStatus(); + if ($actual_status != $expect_status) { + throw new Exception( + pht( + 'Trying to allocate a resource from the wrong status. Status must '. + 'be "%s", actually "%s".', + $expect_status, + $actual_status)); + } + + if ($this->activateWhenAllocated) { + $new_status = DrydockResourceStatus::STATUS_ACTIVE; + } else { + $new_status = DrydockResourceStatus::STATUS_PENDING; + } + $this->openTransaction(); - $statuses = array( - DrydockLeaseStatus::STATUS_PENDING, - DrydockLeaseStatus::STATUS_ACTIVE, - ); - $leases = id(new DrydockLeaseQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withResourceIDs(array($this->getID())) - ->withStatuses($statuses) - ->execute(); + $this + ->setStatus($new_status) + ->save(); - 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(); - } + DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks); + $this->slotLocks = array(); - $this->setStatus(DrydockResourceStatus::STATUS_CLOSED); - $this->save(); $this->saveTransaction(); + + $this->isAllocated = true; + + return $this; + } + + public function isAllocatedResource() { + return $this->isAllocated; + } + + public function activateResource() { + if (!$this->getID()) { + throw new Exception( + pht( + 'Trying to activate a resource which has not yet been persisted.')); + } + + $expect_status = DrydockResourceStatus::STATUS_PENDING; + $actual_status = $this->getStatus(); + if ($actual_status != $expect_status) { + throw new Exception( + pht( + 'Trying to activate a resource from the wrong status. Status must '. + 'be "%s", actually "%s".', + $expect_status, + $actual_status)); + } + + $this->openTransaction(); + + $this + ->setStatus(DrydockResourceStatus::STATUS_ACTIVE) + ->save(); + + DrydockSlotLock::acquireLocks($this->getPHID(), $this->slotLocks); + $this->slotLocks = array(); + + $this->saveTransaction(); + + $this->isActivated = true; + + return $this; + } + + public function isActivatedResource() { + return $this->isActivated; + } + + public function canRelease() { + switch ($this->getStatus()) { + case DrydockResourceStatus::STATUS_RELEASED: + case DrydockResourceStatus::STATUS_DESTROYED: + return false; + default: + return true; + } + } + + public function scheduleUpdate() { + PhabricatorWorker::scheduleTask( + 'DrydockResourceUpdateWorker', + array( + 'resourcePHID' => $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(); + } } @@ -114,21 +218,21 @@ 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: - return PhabricatorPolicies::getMostOpenPolicy(); - } + return $this->getBlueprint()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { - return false; + return $this->getBlueprint()->hasAutomaticCapability( + $capability, + $viewer); } public function describeAutomaticCapability($capability) { - return null; + return pht('Resources inherit the policies of their blueprints.'); } } diff --git a/src/applications/drydock/storage/DrydockSlotLock.php b/src/applications/drydock/storage/DrydockSlotLock.php new file mode 100644 index 0000000000..2f980660aa --- /dev/null +++ b/src/applications/drydock/storage/DrydockSlotLock.php @@ -0,0 +1,175 @@ + false, + self::CONFIG_COLUMN_SCHEMA => array( + 'lockIndex' => 'bytes12', + 'lockKey' => 'text', + ), + self::CONFIG_KEY_SCHEMA => array( + 'key_lock' => array( + 'columns' => array('lockIndex'), + 'unique' => true, + ), + 'key_owner' => array( + 'columns' => array('ownerPHID'), + ), + ), + ) + parent::getConfiguration(); + } + + +/* -( Getting Lock Information )------------------------------------------- */ + + + /** + * Load all locks held by a particular owner. + * + * @param phid Owner PHID. + * @return list All held locks. + * @task info + */ + public static function loadLocks($owner_phid) { + return id(new DrydockSlotLock())->loadAllWhere( + 'ownerPHID = %s', + $owner_phid); + } + + + /** + * Test if a lock is currently free. + * + * @param string Lock key to test. + * @return bool True if the lock is currently free. + * @task info + */ + public static function isLockFree($lock) { + return self::areLocksFree(array($lock)); + } + + + /** + * Test if a list of locks are all currently free. + * + * @param list List of lock keys to test. + * @return bool True if all locks are currently free. + * @task info + */ + public static function areLocksFree(array $locks) { + $lock_map = self::loadHeldLocks($locks); + return !$lock_map; + } + + + /** + * Load named locks. + * + * @param list List of lock keys to load. + * @return list List of held locks. + * @task info + */ + public static function loadHeldLocks(array $locks) { + if (!$locks) { + return array(); + } + + $table = new DrydockSlotLock(); + $conn_r = $table->establishConnection('r'); + + $indexes = array(); + foreach ($locks as $lock) { + $indexes[] = PhabricatorHash::digestForIndex($lock); + } + + return id(new DrydockSlotLock())->loadAllWhere( + 'lockIndex IN (%Ls)', + $indexes); + } + + +/* -( Acquiring and Releasing Locks )-------------------------------------- */ + + + /** + * Acquire a set of slot locks. + * + * This method either acquires all the locks or throws an exception (usually + * because one or more locks are held). + * + * @param phid Lock owner PHID. + * @param list List of locks to acquire. + * @return void + * @task locks + */ + public static function acquireLocks($owner_phid, array $locks) { + if (!$locks) { + return; + } + + $table = new DrydockSlotLock(); + $conn_w = $table->establishConnection('w'); + + $sql = array(); + foreach ($locks as $lock) { + $sql[] = qsprintf( + $conn_w, + '(%s, %s, %s)', + $owner_phid, + PhabricatorHash::digestForIndex($lock), + $lock); + } + + try { + queryfx( + $conn_w, + 'INSERT INTO %T (ownerPHID, lockIndex, lockKey) VALUES %Q', + $table->getTableName(), + implode(', ', $sql)); + } catch (AphrontDuplicateKeyQueryException $ex) { + // Try to improve the readability of the exception. We might miss on + // this query if the lock has already been released, but most of the + // time we should be able to figure out which locks are already held. + $held = self::loadHeldLocks($locks); + $held = mpull($held, 'getOwnerPHID', 'getLockKey'); + throw new DrydockSlotLockException($held); + } + } + + + /** + * Release all locks held by an owner. + * + * @param phid Lock owner PHID. + * @return void + * @task locks + */ + public static function releaseLocks($owner_phid) { + $table = new DrydockSlotLock(); + $conn_w = $table->establishConnection('w'); + + queryfx( + $conn_w, + 'DELETE FROM %T WHERE ownerPHID = %s', + $table->getTableName(), + $owner_phid); + } + +} diff --git a/src/applications/drydock/util/DrydockBlueprintScopeGuard.php b/src/applications/drydock/util/DrydockBlueprintScopeGuard.php deleted file mode 100644 index 343428683b..0000000000 --- a/src/applications/drydock/util/DrydockBlueprintScopeGuard.php +++ /dev/null @@ -1,15 +0,0 @@ -blueprint = $blueprint; - } - - public function __destruct() { - $this->blueprint->popActiveScope(); - } - -} diff --git a/src/applications/drydock/view/DrydockLeaseListView.php b/src/applications/drydock/view/DrydockLeaseListView.php index 7d7d8e77ee..d3507546ad 100644 --- a/src/applications/drydock/view/DrydockLeaseListView.php +++ b/src/applications/drydock/view/DrydockLeaseListView.php @@ -41,7 +41,10 @@ final class DrydockLeaseListView extends AphrontView { $item->addAttribute($status); $item->setEpoch($lease->getDateCreated()); - if ($lease->isActive()) { + // TODO: Tailor this for clarity. + if ($lease->isActivating()) { + $item->setStatusIcon('fa-dot-circle-o yellow'); + } else if ($lease->isActive()) { $item->setStatusIcon('fa-dot-circle-o green'); } else { $item->setStatusIcon('fa-dot-circle-o red'); diff --git a/src/applications/drydock/view/DrydockResourceListView.php b/src/applications/drydock/view/DrydockResourceListView.php index 92ec762b81..9b7706c38b 100644 --- a/src/applications/drydock/view/DrydockResourceListView.php +++ b/src/applications/drydock/view/DrydockResourceListView.php @@ -29,7 +29,7 @@ final class DrydockResourceListView extends AphrontView { case DrydockResourceStatus::STATUS_PENDING: $item->setStatusIcon('fa-dot-circle-o yellow'); break; - case DrydockResourceStatus::STATUS_OPEN: + case DrydockResourceStatus::STATUS_ACTIVE: $item->setStatusIcon('fa-dot-circle-o green'); break; case DrydockResourceStatus::STATUS_DESTROYED: diff --git a/src/applications/drydock/worker/DrydockAllocatorWorker.php b/src/applications/drydock/worker/DrydockAllocatorWorker.php index f9a647a3a8..a67f242c01 100644 --- a/src/applications/drydock/worker/DrydockAllocatorWorker.php +++ b/src/applications/drydock/worker/DrydockAllocatorWorker.php @@ -1,187 +1,479 @@ lease)) { - $lease = id(new DrydockLeaseQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withIDs(array($this->getTaskData())) - ->executeOne(); - if (!$lease) { - throw new PhabricatorWorkerPermanentFailureException( - pht('No such lease %d!', $this->getTaskData())); - } - $this->lease = $lease; - } - return $this->lease; - } - - private function logToDrydock($message) { - DrydockBlueprintImplementation::writeLog( - null, - $this->loadLease(), - $message); - } +/** + * @task allocate Allocator + * @task resource Managing Resources + * @task lease Managing Leases + */ +final class DrydockAllocatorWorker extends DrydockWorker { protected function doWork() { - $lease = $this->loadLease(); - $this->logToDrydock(pht('Allocating Lease')); + $lease_phid = $this->getTaskDataValue('leasePHID'); + $lease = $this->loadLease($lease_phid); - try { - $this->allocateLease($lease); - } catch (Exception $ex) { + $this->allocateAndAcquireLease($lease); + } - // TODO: We should really do this when archiving the task, if we've - // suffered a permanent failure. But we don't have hooks for that yet - // and always fail after the first retry right now, so this is - // functionally equivalent. - $lease->reload(); - if ($lease->getStatus() == DrydockLeaseStatus::STATUS_PENDING) { - $lease->setStatus(DrydockLeaseStatus::STATUS_BROKEN); - $lease->save(); + +/* -( Allocator )---------------------------------------------------------- */ + + + /** + * Find or build a resource which can satisfy a given lease request, then + * acquire the lease. + * + * @param DrydockLease Requested lease. + * @return void + * @task allocator + */ + private function allocateAndAcquireLease(DrydockLease $lease) { + $blueprints = $this->loadBlueprintsForAllocatingLease($lease); + + // If we get nothing back, that means no blueprint is defined which can + // ever build the requested resource. This is a permanent failure, since + // we don't expect to succeed no matter how many times we try. + if (!$blueprints) { + $lease + ->setStatus(DrydockLeaseStatus::STATUS_BROKEN) + ->save(); + throw new PhabricatorWorkerPermanentFailureException( + pht( + 'No active Drydock blueprint exists which can ever allocate a '. + 'resource for lease "%s".', + $lease->getPHID())); + } + + // First, try to find a suitable open resource which we can acquire a new + // lease on. + $resources = $this->loadResourcesForAllocatingLease($blueprints, $lease); + + // If no resources exist yet, see if we can build one. + if (!$resources) { + $usable_blueprints = $this->removeOverallocatedBlueprints( + $blueprints, + $lease); + + // If we get nothing back here, some blueprint claims it can eventually + // satisfy the lease, just not right now. This is a temporary failure, + // and we expect allocation to succeed eventually. + if (!$blueprints) { + // TODO: More formal temporary failure here. We should retry this + // "soon" but not "immediately". + throw new Exception( + pht('No blueprints have space to allocate a resource right now.')); } - throw $ex; + $usable_blueprints = $this->rankBlueprints($blueprints, $lease); + + $exceptions = array(); + foreach ($usable_blueprints as $blueprint) { + try { + $resources[] = $this->allocateResource($blueprint, $lease); + + // Bail after allocating one resource, we don't need any more than + // this. + break; + } catch (Exception $ex) { + $exceptions[] = $ex; + } + } + + if (!$resources) { + // TODO: We should distinguish between temporary and permament failures + // here. If any blueprint failed temporarily, retry "soon". If none + // of these failures were temporary, maybe this should be a permanent + // failure? + throw new PhutilAggregateException( + pht( + 'All blueprints failed to allocate a suitable new resource when '. + 'trying to allocate lease "%s".', + $lease->getPHID()), + $exceptions); + } + + // NOTE: We have not acquired the lease yet, so it is possible that the + // resource we just built will be snatched up by some other lease before + // we can. This is not problematic: we'll retry a little later and should + // suceed eventually. + } + + $resources = $this->rankResources($resources, $lease); + + $exceptions = array(); + $allocated = false; + foreach ($resources as $resource) { + try { + $this->acquireLease($resource, $lease); + $allocated = true; + break; + } catch (Exception $ex) { + $exceptions[] = $ex; + } + } + + if (!$allocated) { + // TODO: We should distinguish between temporary and permanent failures + // here. If any failures were temporary (specifically, failed to acquire + // locks) + + throw new PhutilAggregateException( + pht( + 'Unable to acquire lease "%s" on any resouce.', + $lease->getPHID()), + $exceptions); } } - private function loadAllBlueprints() { - $viewer = PhabricatorUser::getOmnipotentUser(); - $instances = id(new DrydockBlueprintQuery()) - ->setViewer($viewer) - ->execute(); - $blueprints = array(); - foreach ($instances as $instance) { - $blueprints[$instance->getPHID()] = $instance; - } - return $blueprints; - } - private function allocateLease(DrydockLease $lease) { - $type = $lease->getResourceType(); + /** + * Get all the @{class:DrydockBlueprintImplementation}s which can possibly + * build a resource to satisfy a lease. + * + * This method returns blueprints which might, at some time, be able to + * build a resource which can satisfy the lease. They may not be able to + * build that resource right now. + * + * @param DrydockLease Requested lease. + * @return list List of qualifying blueprint + * implementations. + * @task allocator + */ + private function loadBlueprintImplementationsForAllocatingLease( + DrydockLease $lease) { - $blueprints = $this->loadAllBlueprints(); + $impls = DrydockBlueprintImplementation::getAllBlueprintImplementations(); - // TODO: Policy stuff. - $pool = id(new DrydockResource())->loadAllWhere( - 'type = %s AND status = %s', - $lease->getResourceType(), - DrydockResourceStatus::STATUS_OPEN); - - $this->logToDrydock( - pht('Found %d Open Resource(s)', count($pool))); - - $candidates = array(); - foreach ($pool as $key => $candidate) { - if (!isset($blueprints[$candidate->getBlueprintPHID()])) { - unset($pool[$key]); + $keep = array(); + foreach ($impls as $key => $impl) { + // Don't use disabled blueprint types. + if (!$impl->isEnabled()) { continue; } - $blueprint = $blueprints[$candidate->getBlueprintPHID()]; - $implementation = $blueprint->getImplementation(); - - if ($implementation->filterResource($candidate, $lease)) { - $candidates[] = $candidate; + // Don't use blueprint types which can't allocate the correct kind of + // resource. + if ($impl->getType() != $lease->getResourceType()) { + continue; } + + if (!$impl->canAnyBlueprintEverAllocateResourceForLease($lease)) { + continue; + } + + $keep[$key] = $impl; } - $this->logToDrydock(pht('%d Open Resource(s) Remain', count($candidates))); + return $keep; + } - $resource = null; - if ($candidates) { - shuffle($candidates); - foreach ($candidates as $candidate_resource) { - $blueprint = $blueprints[$candidate_resource->getBlueprintPHID()] - ->getImplementation(); - if ($blueprint->allocateLease($candidate_resource, $lease)) { - $resource = $candidate_resource; - break; - } - } + + /** + * Get all the concrete @{class:DrydockBlueprint}s which can possibly + * build a resource to satisfy a lease. + * + * @param DrydockLease Requested lease. + * @return list List of qualifying blueprints. + * @task allocator + */ + private function loadBlueprintsForAllocatingLease( + DrydockLease $lease) { + $viewer = $this->getViewer(); + + $impls = $this->loadBlueprintImplementationsForAllocatingLease($lease); + if (!$impls) { + return array(); } - if (!$resource) { - $blueprints = DrydockBlueprintImplementation - ::getAllBlueprintImplementationsForResource($type); + $blueprints = id(new DrydockBlueprintQuery()) + ->setViewer($viewer) + ->withBlueprintClasses(array_keys($impls)) + ->withDisabled(false) + ->execute(); - $this->logToDrydock( - pht('Found %d Blueprints', count($blueprints))); - - foreach ($blueprints as $key => $candidate_blueprint) { - if (!$candidate_blueprint->isEnabled()) { - unset($blueprints[$key]); - continue; - } + $keep = array(); + foreach ($blueprints as $key => $blueprint) { + if (!$blueprint->canEverAllocateResourceForLease($lease)) { + continue; } - $this->logToDrydock( - pht('%d Blueprints Enabled', count($blueprints))); - - foreach ($blueprints as $key => $candidate_blueprint) { - if (!$candidate_blueprint->canAllocateMoreResources($pool)) { - unset($blueprints[$key]); - continue; - } - } - - $this->logToDrydock( - pht('%d Blueprints Can Allocate', count($blueprints))); - - if (!$blueprints) { - $lease->setStatus(DrydockLeaseStatus::STATUS_BROKEN); - $lease->save(); - - $this->logToDrydock( - pht( - "There are no resources of type '%s' available, and no ". - "blueprints which can allocate new ones.", - $type)); - - return; - } - - // TODO: Rank intelligently. - shuffle($blueprints); - - $blueprint = head($blueprints); - $resource = $blueprint->allocateResource($lease); - - if (!$blueprint->allocateLease($resource, $lease)) { - // TODO: This "should" happen only if we lost a race with another lease, - // which happened to acquire this resource immediately after we - // allocated it. In this case, the right behavior is to retry - // immediately. However, other things like a blueprint allocating a - // resource it can't actually allocate the lease on might be happening - // too, in which case we'd just allocate infinite resources. Probably - // what we should do is test for an active or allocated lease and retry - // if we find one (although it might have already been released by now) - // and fail really hard ("your configuration is a huge broken mess") - // otherwise. But just throw for now since this stuff is all edge-casey. - // Alternatively we could bring resources up in a "BESPOKE" status - // and then switch them to "OPEN" only after the allocating lease gets - // its grubby mitts on the resource. This might make more sense but - // is a bit messy. - throw new Exception(pht('Lost an allocation race?')); - } + $keep[$key] = $blueprint; } + return $keep; + } + + + /** + * Load a list of all resources which a given lease can possibly be + * allocated against. + * + * @param list Blueprints which may produce suitable + * resources. + * @param DrydockLease Requested lease. + * @return list Resources which may be able to allocate + * the lease. + * @task allocator + */ + private function loadResourcesForAllocatingLease( + array $blueprints, + DrydockLease $lease) { + assert_instances_of($blueprints, 'DrydockBlueprint'); + $viewer = $this->getViewer(); + + $resources = id(new DrydockResourceQuery()) + ->setViewer($viewer) + ->withBlueprintPHIDs(mpull($blueprints, 'getPHID')) + ->withTypes(array($lease->getResourceType())) + ->withStatuses( + array( + DrydockResourceStatus::STATUS_PENDING, + DrydockResourceStatus::STATUS_ACTIVE, + )) + ->execute(); + + $keep = array(); + foreach ($resources as $key => $resource) { + $blueprint = $resource->getBlueprint(); + + if (!$blueprint->canAcquireLeaseOnResource($resource, $lease)) { + continue; + } + + $keep[$key] = $resource; + } + + return $keep; + } + + + /** + * Remove blueprints which are too heavily allocated to build a resource for + * a lease from a list of blueprints. + * + * @param list List of blueprints. + * @return list List with blueprints that can not allocate + * a resource for the lease right now removed. + * @task allocator + */ + private function removeOverallocatedBlueprints( + array $blueprints, + DrydockLease $lease) { + assert_instances_of($blueprints, 'DrydockBlueprint'); + + $keep = array(); + foreach ($blueprints as $key => $blueprint) { + if (!$blueprint->canAllocateResourceForLease($lease)) { + continue; + } + + $keep[$key] = $blueprint; + } + + return $keep; + } + + + /** + * Rank blueprints by suitability for building a new resource for a + * particular lease. + * + * @param list List of blueprints. + * @param DrydockLease Requested lease. + * @return list Ranked list of blueprints. + * @task allocator + */ + private function rankBlueprints(array $blueprints, DrydockLease $lease) { + assert_instances_of($blueprints, 'DrydockBlueprint'); + + // TODO: Implement improvements to this ranking algorithm if they become + // available. + shuffle($blueprints); + + return $blueprints; + } + + + /** + * Rank resources by suitability for allocating a particular lease. + * + * @param list List of resources. + * @param DrydockLease Requested lease. + * @return list Ranked list of resources. + * @task allocator + */ + private function rankResources(array $resources, DrydockLease $lease) { + assert_instances_of($resources, 'DrydockResource'); + + // TODO: Implement improvements to this ranking algorithm if they become + // available. + shuffle($resources); + + return $resources; + } + + +/* -( Managing Resources )------------------------------------------------- */ + + + /** + * Perform an actual resource allocation with a particular blueprint. + * + * @param DrydockBlueprint The blueprint to allocate a resource from. + * @param DrydockLease Requested lease. + * @return DrydockResource Allocated resource. + * @task resource + */ + private function allocateResource( + DrydockBlueprint $blueprint, + DrydockLease $lease) { + $resource = $blueprint->allocateResource($lease); + $this->validateAllocatedResource($blueprint, $resource, $lease); + + // If this resource was allocated as a pending resource, queue a task to + // activate it. + if ($resource->getStatus() == DrydockResourceStatus::STATUS_PENDING) { + PhabricatorWorker::scheduleTask( + 'DrydockResourceWorker', + array( + 'resourcePHID' => $resource->getPHID(), + ), + array( + 'objectPHID' => $resource->getPHID(), + )); + } + + return $resource; + } + + + /** + * Check that the resource a blueprint allocated is roughly the sort of + * object we expect. + * + * @param DrydockBlueprint Blueprint which built the resource. + * @param wild Thing which the blueprint claims is a valid resource. + * @param DrydockLease Lease the resource was allocated for. + * @return void + * @task resource + */ + private function validateAllocatedResource( + DrydockBlueprint $blueprint, + $resource, + DrydockLease $lease) { + + if (!($resource instanceof DrydockResource)) { + throw new Exception( + pht( + 'Blueprint "%s" (of type "%s") is not properly implemented: %s must '. + 'return an object of type %s or throw, but returned something else.', + $blueprint->getBlueprintName(), + $blueprint->getClassName(), + 'allocateResource()', + 'DrydockResource')); + } + + if (!$resource->isAllocatedResource()) { + throw new Exception( + pht( + 'Blueprint "%s" (of type "%s") is not properly implemented: %s '. + 'must actually allocate the resource it returns.', + $blueprint->getBlueprintName(), + $blueprint->getClassName(), + 'allocateResource()')); + } + + $resource_type = $resource->getType(); + $lease_type = $lease->getResourceType(); + + if ($resource_type !== $lease_type) { + // TODO: Destroy the resource here? + + throw new Exception( + pht( + 'Blueprint "%s" (of type "%s") is not properly implemented: it '. + 'built a resource of type "%s" to satisfy a lease requesting a '. + 'resource of type "%s".', + $blueprint->getBlueprintName(), + $blueprint->getClassName(), + $resource_type, + $lease_type)); + } + } + + +/* -( Managing Leases )---------------------------------------------------- */ + + + /** + * Perform an actual lease acquisition on a particular resource. + * + * @param DrydockResource Resource to acquire a lease on. + * @param DrydockLease Lease to acquire. + * @return void + * @task lease + */ + private function acquireLease( + DrydockResource $resource, + DrydockLease $lease) { + $blueprint = $resource->getBlueprint(); $blueprint->acquireLease($resource, $lease); + + $this->validateAcquiredLease($blueprint, $resource, $lease); + + // If this lease has been acquired but not activated, queue a task to + // activate it. + if ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACQUIRED) { + PhabricatorWorker::scheduleTask( + 'DrydockLeaseWorker', + array( + 'leasePHID' => $lease->getPHID(), + ), + array( + 'objectPHID' => $lease->getPHID(), + )); + } } + + /** + * Make sure that a lease was really acquired properly. + * + * @param DrydockBlueprint Blueprint which created the resource. + * @param DrydockResource Resource which was acquired. + * @param DrydockLease The lease which was supposedly acquired. + * @return void + * @task lease + */ + private function validateAcquiredLease( + DrydockBlueprint $blueprint, + DrydockResource $resource, + DrydockLease $lease) { + + if (!$lease->isAcquiredLease()) { + throw new Exception( + pht( + 'Blueprint "%s" (of type "%s") is not properly implemented: it '. + 'returned from "%s" without acquiring a lease.', + $blueprint->getBlueprintName(), + $blueprint->getClassName(), + 'acquireLease()')); + } + + $lease_phid = $lease->getResourcePHID(); + $resource_phid = $resource->getPHID(); + + if ($lease_phid !== $resource_phid) { + // TODO: Destroy the lease? + throw new Exception( + pht( + 'Blueprint "%s" (of type "%s") is not properly implemented: it '. + 'returned from "%s" with a lease acquired on the wrong resource.', + $blueprint->getBlueprintName(), + $blueprint->getClassName(), + 'acquireLease()')); + } + } + + } diff --git a/src/applications/drydock/worker/DrydockLeaseDestroyWorker.php b/src/applications/drydock/worker/DrydockLeaseDestroyWorker.php new file mode 100644 index 0000000000..e0b15095c7 --- /dev/null +++ b/src/applications/drydock/worker/DrydockLeaseDestroyWorker.php @@ -0,0 +1,37 @@ +getTaskDataValue('leasePHID'); + $lease = $this->loadLease($lease_phid); + $this->destroyLease($lease); + } + + private function destroyLease(DrydockLease $lease) { + $status = $lease->getStatus(); + + switch ($status) { + case DrydockLeaseStatus::STATUS_RELEASED: + case DrydockLeaseStatus::STATUS_BROKEN: + break; + default: + throw new PhabricatorWorkerPermanentFailureException( + pht( + 'Unable to destroy lease ("%s"), lease has the wrong '. + 'status ("%s").', + $lease->getPHID(), + $status)); + } + + $resource = $lease->getResource(); + $blueprint = $resource->getBlueprint(); + + $blueprint->destroyLease($resource, $lease); + + $lease + ->setStatus(DrydockLeaseStatus::STATUS_DESTROYED) + ->save(); + } + +} diff --git a/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php b/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php new file mode 100644 index 0000000000..98ecf2b498 --- /dev/null +++ b/src/applications/drydock/worker/DrydockLeaseUpdateWorker.php @@ -0,0 +1,107 @@ +getTaskDataValue('leasePHID'); + + $hash = PhabricatorHash::digestForIndex($lease_phid); + $lock_key = 'drydock.lease:'.$hash; + + $lock = PhabricatorGlobalLock::newLock($lock_key) + ->lock(1); + + try { + $lease = $this->loadLease($lease_phid); + $this->updateLease($lease); + } catch (Exception $ex) { + $lock->unlock(); + throw $ex; + } + + $lock->unlock(); + } + + private function updateLease(DrydockLease $lease) { + if ($lease->getStatus() != DrydockLeaseStatus::STATUS_ACTIVE) { + return; + } + + $viewer = $this->getViewer(); + $drydock_phid = id(new PhabricatorDrydockApplication())->getPHID(); + + // Check if the lease has expired. If it is, we're going to send it a + // release command. This command will be handled immediately below, it + // just generates a command log and improves consistency. + $now = PhabricatorTime::getNow(); + $expires = $lease->getUntil(); + if ($expires && ($expires <= $now)) { + $command = DrydockCommand::initializeNewCommand($viewer) + ->setTargetPHID($lease->getPHID()) + ->setAuthorPHID($drydock_phid) + ->setCommand(DrydockCommand::COMMAND_RELEASE) + ->save(); + } + + $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(); + } + + // If this is the task which will eventually release the lease after it + // expires but it is still active, reschedule the task to run after the + // lease expires. This can happen if the lease's expiration was pushed + // forward. + if ($lease->getStatus() == DrydockLeaseStatus::STATUS_ACTIVE) { + if ($this->getTaskDataValue('isExpireTask') && $expires) { + throw new PhabricatorWorkerYieldException($expires - $now); + } + } + } + + 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(); + + PhabricatorWorker::scheduleTask( + 'DrydockLeaseDestroyWorker', + array( + 'leasePHID' => $lease->getPHID(), + ), + array( + 'objectPHID' => $lease->getPHID(), + )); + + $resource = $lease->getResource(); + $blueprint = $resource->getBlueprint(); + + $blueprint->didReleaseLease($resource, $lease); + } + +} diff --git a/src/applications/drydock/worker/DrydockLeaseWorker.php b/src/applications/drydock/worker/DrydockLeaseWorker.php new file mode 100644 index 0000000000..5919c59613 --- /dev/null +++ b/src/applications/drydock/worker/DrydockLeaseWorker.php @@ -0,0 +1,74 @@ +getTaskDataValue('leasePHID'); + $lease = $this->loadLease($lease_phid); + + $this->activateLease($lease); + } + + + private function activateLease(DrydockLease $lease) { + $actual_status = $lease->getStatus(); + + if ($actual_status != DrydockLeaseStatus::STATUS_ACQUIRED) { + throw new PhabricatorWorkerPermanentFailureException( + pht( + 'Trying to activate lease from wrong status ("%s").', + $actual_status)); + } + + $resource = $lease->getResource(); + if (!$resource) { + throw new PhabricatorWorkerPermanentFailureException( + pht('Trying to activate lease with no resource.')); + } + + $resource_status = $resource->getStatus(); + + if ($resource_status == DrydockResourceStatus::STATUS_PENDING) { + // TODO: This is explicitly a temporary failure -- we are waiting for + // the resource to come up. + throw new Exception(pht('Resource still activating.')); + } + + if ($resource_status != DrydockResourceStatus::STATUS_ACTIVE) { + throw new PhabricatorWorkerPermanentFailureException( + pht( + 'Trying to activate lease on a dead resource (in status "%s").', + $resource_status)); + } + + // NOTE: We can race resource destruction here. Between the time we + // performed the read above and now, the resource might have closed, so + // we may activate leases on dead resources. At least for now, this seems + // fine: a resource dying right before we activate a lease on it should not + // be distinguisahble from a resource dying right after we activate a lease + // on it. We end up with an active lease on a dead resource either way, and + // can not prevent resources dying from lightning strikes. + + $blueprint = $resource->getBlueprint(); + $blueprint->activateLease($resource, $lease); + $this->validateActivatedLease($blueprint, $resource, $lease); + } + + private function validateActivatedLease( + DrydockBlueprint $blueprint, + DrydockResource $resource, + DrydockLease $lease) { + + if (!$lease->isActivatedLease()) { + throw new Exception( + pht( + 'Blueprint "%s" (of type "%s") is not properly implemented: it '. + 'returned from "%s" without activating a lease.', + $blueprint->getBlueprintName(), + $blueprint->getClassName(), + 'acquireLease()')); + } + + } + +} diff --git a/src/applications/drydock/worker/DrydockResourceDestroyWorker.php b/src/applications/drydock/worker/DrydockResourceDestroyWorker.php new file mode 100644 index 0000000000..af00ebeb2a --- /dev/null +++ b/src/applications/drydock/worker/DrydockResourceDestroyWorker.php @@ -0,0 +1,35 @@ +getTaskDataValue('resourcePHID'); + $resource = $this->loadResource($resource_phid); + $this->destroyResource($resource); + } + + private function destroyResource(DrydockResource $resource) { + $status = $resource->getStatus(); + + switch ($status) { + case DrydockResourceStatus::STATUS_RELEASED: + case DrydockResourceStatus::STATUS_BROKEN: + break; + default: + throw new PhabricatorWorkerPermanentFailureException( + pht( + 'Unable to destroy resource ("%s"), resource has the wrong '. + 'status ("%s").', + $resource->getPHID(), + $status)); + } + + $blueprint = $resource->getBlueprint(); + $blueprint->destroyResource($resource); + + $resource + ->setStatus(DrydockResourceStatus::STATUS_DESTROYED) + ->save(); + } + +} diff --git a/src/applications/drydock/worker/DrydockResourceUpdateWorker.php b/src/applications/drydock/worker/DrydockResourceUpdateWorker.php new file mode 100644 index 0000000000..4afc51bc23 --- /dev/null +++ b/src/applications/drydock/worker/DrydockResourceUpdateWorker.php @@ -0,0 +1,99 @@ +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_ACTIVE) { + // 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_ACTIVE) { + // 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_RELEASED) + ->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) + ->withResourcePHIDs(array($resource->getPHID())) + ->withStatuses($statuses) + ->execute(); + + foreach ($leases as $lease) { + $command = DrydockCommand::initializeNewCommand($viewer) + ->setTargetPHID($lease->getPHID()) + ->setAuthorPHID($drydock_phid) + ->setCommand(DrydockCommand::COMMAND_RELEASE) + ->save(); + + $lease->scheduleUpdate(); + } + + PhabricatorWorker::scheduleTask( + 'DrydockResourceDestroyWorker', + array( + 'resourcePHID' => $resource->getPHID(), + ), + array( + 'objectPHID' => $resource->getPHID(), + )); + } + +} diff --git a/src/applications/drydock/worker/DrydockResourceWorker.php b/src/applications/drydock/worker/DrydockResourceWorker.php new file mode 100644 index 0000000000..45d63029f7 --- /dev/null +++ b/src/applications/drydock/worker/DrydockResourceWorker.php @@ -0,0 +1,45 @@ +getTaskDataValue('resourcePHID'); + $resource = $this->loadResource($resource_phid); + + $this->activateResource($resource); + } + + + private function activateResource(DrydockResource $resource) { + $resource_status = $resource->getStatus(); + + if ($resource_status != DrydockResourceStatus::STATUS_PENDING) { + throw new PhabricatorWorkerPermanentFailureException( + pht( + 'Trying to activate resource from wrong status ("%s").', + $resource_status)); + } + + $blueprint = $resource->getBlueprint(); + $blueprint->activateResource($resource); + $this->validateActivatedResource($blueprint, $resource); + } + + + private function validateActivatedResource( + DrydockBlueprint $blueprint, + DrydockResource $resource) { + + if (!$resource->isActivatedResource()) { + throw new Exception( + pht( + 'Blueprint "%s" (of type "%s") is not properly implemented: %s '. + 'must actually allocate the resource it returns.', + $blueprint->getBlueprintName(), + $blueprint->getClassName(), + 'allocateResource()')); + } + + } + +} diff --git a/src/applications/drydock/worker/DrydockWorker.php b/src/applications/drydock/worker/DrydockWorker.php new file mode 100644 index 0000000000..d41643de47 --- /dev/null +++ b/src/applications/drydock/worker/DrydockWorker.php @@ -0,0 +1,53 @@ +getViewer(); + + $lease = id(new DrydockLeaseQuery()) + ->setViewer($viewer) + ->withPHIDs(array($lease_phid)) + ->executeOne(); + if (!$lease) { + throw new PhabricatorWorkerPermanentFailureException( + pht('No such lease "%s"!', $lease_phid)); + } + + return $lease; + } + + protected function loadResource($resource_phid) { + $viewer = $this->getViewer(); + + $resource = id(new DrydockResourceQuery()) + ->setViewer($viewer) + ->withPHIDs(array($resource_phid)) + ->executeOne(); + if (!$resource) { + throw new PhabricatorWorkerPermanentFailureException( + pht('No such resource "%s"!', $resource_phid)); + } + + 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/files/controller/PhabricatorFileTransformListController.php b/src/applications/files/controller/PhabricatorFileTransformListController.php index 6ef1818962..767254ad3a 100644 --- a/src/applications/files/controller/PhabricatorFileTransformListController.php +++ b/src/applications/files/controller/PhabricatorFileTransformListController.php @@ -116,11 +116,11 @@ final class PhabricatorFileTransformListController $dst_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('File Sources')) - ->appendChild($dst_table); + ->setTable($dst_table); $src_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Available Transforms')) - ->appendChild($src_table); + ->setTable($src_table); return $this->buildApplicationPage( array( diff --git a/src/applications/fund/controller/FundInitiativeViewController.php b/src/applications/fund/controller/FundInitiativeViewController.php index 3d12f1549c..71cebc842d 100644 --- a/src/applications/fund/controller/FundInitiativeViewController.php +++ b/src/applications/fund/controller/FundInitiativeViewController.php @@ -103,7 +103,8 @@ final class FundInitiativeViewController 'default', $viewer); - $view->addSectionHeader(pht('Description')); + $view->addSectionHeader( + pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $view->addTextContent($description); } @@ -114,7 +115,8 @@ final class FundInitiativeViewController 'default', $viewer); - $view->addSectionHeader(pht('Risks/Challenges')); + $view->addSectionHeader( + pht('Risks/Challenges'), 'fa-ambulance'); $view->addTextContent($risks); } diff --git a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php index 266ac4fafc..59b8ce6444 100644 --- a/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php +++ b/src/applications/harbormaster/application/PhabricatorHarbormasterApplication.php @@ -65,12 +65,13 @@ final class PhabricatorHarbormasterApplication extends PhabricatorApplication { 'delete/(?:(?P\d+)/)?' => 'HarbormasterStepDeleteController', ), 'buildable/' => array( - '(?P\d+)/(?Pstop|resume|restart)/' + '(?P\d+)/(?Ppause|resume|restart|abort)/' => 'HarbormasterBuildableActionController', ), 'build/' => array( '(?P\d+)/' => 'HarbormasterBuildViewController', - '(?Pstop|resume|restart)/(?P\d+)/(?:(?P[^/]+)/)?' + '(?Ppause|resume|restart|abort)/'. + '(?P\d+)/(?:(?P[^/]+)/)?' => 'HarbormasterBuildActionController', ), 'plan/' => array( diff --git a/src/applications/harbormaster/artifact/HarbormasterDrydockLeaseArtifact.php b/src/applications/harbormaster/artifact/HarbormasterDrydockLeaseArtifact.php new file mode 100644 index 0000000000..bd3868e7db --- /dev/null +++ b/src/applications/harbormaster/artifact/HarbormasterDrydockLeaseArtifact.php @@ -0,0 +1,73 @@ + 'string', + ); + } + + public function getArtifactParameterDescriptions() { + return array( + 'drydockLeasePHID' => pht( + 'Drydock working copy lease to create an artifact from.'), + ); + } + + public function getArtifactDataExample() { + return array( + 'drydockLeasePHID' => 'PHID-DRYL-abcdefghijklmnopqrst', + ); + } + + public function renderArtifactSummary(PhabricatorUser $viewer) { + $artifact = $this->getBuildArtifact(); + $lease_phid = $artifact->getProperty('drydockLeasePHID'); + return $viewer->renderHandle($lease_phid); + } + + public function willCreateArtifact(PhabricatorUser $actor) { + $this->loadArtifactLease($actor); + } + + public function loadArtifactLease(PhabricatorUser $viewer) { + $artifact = $this->getBuildArtifact(); + $lease_phid = $artifact->getProperty('drydockLeasePHID'); + + $lease = id(new DrydockLeaseQuery()) + ->setViewer($viewer) + ->withPHIDs(array($lease_phid)) + ->executeOne(); + if (!$lease) { + throw new Exception( + pht( + 'Drydock lease PHID "%s" does not correspond to a valid lease.', + $lease_phid)); + } + + return $lease; + } + + public function releaseArtifact(PhabricatorUser $actor) { + $lease = $this->loadArtifactLease($actor); + if (!$lease->canRelease()) { + return; + } + + $author_phid = $actor->getPHID(); + if (!$author_phid) { + $author_phid = id(new PhabricatorHarbormasterApplication())->getPHID(); + } + + $command = DrydockCommand::initializeNewCommand($actor) + ->setTargetPHID($lease->getPHID()) + ->setAuthorPHID($author_phid) + ->setCommand(DrydockCommand::COMMAND_RELEASE) + ->save(); + + $lease->scheduleUpdate(); + } + +} diff --git a/src/applications/harbormaster/artifact/HarbormasterHostArtifact.php b/src/applications/harbormaster/artifact/HarbormasterHostArtifact.php index 73d00844af..1c2eec8264 100644 --- a/src/applications/harbormaster/artifact/HarbormasterHostArtifact.php +++ b/src/applications/harbormaster/artifact/HarbormasterHostArtifact.php @@ -1,6 +1,7 @@ 'string', - ); - } - - public function getArtifactParameterDescriptions() { - return array( - 'drydockLeasePHID' => pht( - 'Drydock host lease to create an artifact from.'), - ); - } - - public function getArtifactDataExample() { - return array( - 'drydockLeasePHID' => 'PHID-DRYL-abcdefghijklmnopqrst', - ); - } - - public function renderArtifactSummary(PhabricatorUser $viewer) { - $artifact = $this->getBuildArtifact(); - $file_phid = $artifact->getProperty('drydockLeasePHID'); - return $viewer->renderHandle($file_phid); - } - - public function willCreateArtifact(PhabricatorUser $actor) { - $this->loadArtifactLease($actor); - } - - public function loadArtifactLease(PhabricatorUser $viewer) { - $artifact = $this->getBuildArtifact(); - $lease_phid = $artifact->getProperty('drydockLeasePHID'); - - $lease = id(new DrydockLeaseQuery()) - ->setViewer($viewer) - ->withPHIDs(array($lease_phid)) - ->executeOne(); - if (!$lease) { - throw new Exception( - pht( - 'Drydock lease PHID "%s" does not correspond to a valid lease.', - $lease_phid)); - } - - return $lease; - } - - public function releaseArtifact(PhabricatorUser $actor) { - $lease = $this->loadArtifactLease($actor); - $resource = $lease->getResource(); - $blueprint = $resource->getBlueprint(); - - if ($lease->isActive()) { - $blueprint->releaseLease($resource, $lease); - } - } - - } diff --git a/src/applications/harbormaster/artifact/HarbormasterWorkingCopyArtifact.php b/src/applications/harbormaster/artifact/HarbormasterWorkingCopyArtifact.php new file mode 100644 index 0000000000..84ce403124 --- /dev/null +++ b/src/applications/harbormaster/artifact/HarbormasterWorkingCopyArtifact.php @@ -0,0 +1,16 @@ +canRestartBuild(); break; - case HarbormasterBuildCommand::COMMAND_STOP: - $can_issue = $build->canStopBuild(); + case HarbormasterBuildCommand::COMMAND_PAUSE: + $can_issue = $build->canPauseBuild(); break; case HarbormasterBuildCommand::COMMAND_RESUME: $can_issue = $build->canResumeBuild(); break; + case HarbormasterBuildCommand::COMMAND_ABORT: + $can_issue = $build->canAbortBuild(); + break; default: return new Aphront400Response(); } @@ -90,7 +93,19 @@ final class HarbormasterBuildActionController } } break; - case HarbormasterBuildCommand::COMMAND_STOP: + case HarbormasterBuildCommand::COMMAND_ABORT: + if ($can_issue) { + $title = pht('Really abort build?'); + $body = pht( + 'Progress on this build will be discarded. Really '. + 'abort build?'); + $submit = pht('Abort Build'); + } else { + $title = pht('Unable to Abort Build'); + $body = pht('You can not abort this build.'); + } + break; + case HarbormasterBuildCommand::COMMAND_PAUSE: if ($can_issue) { $title = pht('Really pause build?'); $body = pht( @@ -103,11 +118,11 @@ final class HarbormasterBuildActionController $body = pht( 'This build is already complete. You can not pause a completed '. 'build.'); - } else if ($build->isStopped()) { + } else if ($build->isPaused()) { $body = pht( 'This build is already paused. You can not pause a build which '. 'has already been paused.'); - } else if ($build->isStopping()) { + } else if ($build->isPausing()) { $body = pht( 'This build is already pausing. You can not reissue a pause '. 'command to a pausing build.'); @@ -129,9 +144,9 @@ final class HarbormasterBuildActionController $body = pht( 'This build is already resuming. You can not reissue a resume '. 'command to a resuming build.'); - } else if (!$build->isStopped()) { + } else if (!$build->isPaused()) { $body = pht( - 'This build is not stopped. You can only resume a stopped '. + 'This build is not paused. You can only resume a paused '. 'build.'); } } diff --git a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php index 1a9832c588..1d655bb2e0 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php @@ -29,10 +29,12 @@ final class HarbormasterBuildViewController if ($build->isRestarting()) { $header->setStatus('fa-exclamation-triangle', 'red', pht('Restarting')); - } else if ($build->isStopping()) { + } else if ($build->isPausing()) { $header->setStatus('fa-exclamation-triangle', 'red', pht('Pausing')); } else if ($build->isResuming()) { $header->setStatus('fa-exclamation-triangle', 'red', pht('Resuming')); + } else if ($build->isAborting()) { + $header->setStatus('fa-exclamation-triangle', 'red', pht('Aborting')); } $box = id(new PHUIObjectBoxView()) @@ -186,7 +188,8 @@ final class HarbormasterBuildViewController 'default', $viewer); - $properties->addSectionHeader(pht('Description')); + $properties->addSectionHeader( + pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $properties->addTextContent($rendered); } } else { @@ -446,8 +449,9 @@ final class HarbormasterBuildViewController ->setObjectURI("/build/{$id}"); $can_restart = $build->canRestartBuild(); - $can_stop = $build->canStopBuild(); + $can_pause = $build->canPauseBuild(); $can_resume = $build->canResumeBuild(); + $can_abort = $build->canAbortBuild(); $list->addAction( id(new PhabricatorActionView()) @@ -470,11 +474,19 @@ final class HarbormasterBuildViewController id(new PhabricatorActionView()) ->setName(pht('Pause Build')) ->setIcon('fa-pause') - ->setHref($this->getApplicationURI('/build/stop/'.$id.'/')) - ->setDisabled(!$can_stop) + ->setHref($this->getApplicationURI('/build/pause/'.$id.'/')) + ->setDisabled(!$can_pause) ->setWorkflow(true)); } + $list->addAction( + id(new PhabricatorActionView()) + ->setName(pht('Abort Build')) + ->setIcon('fa-exclamation-triangle') + ->setHref($this->getApplicationURI('/build/abort/'.$id.'/')) + ->setDisabled(!$can_abort) + ->setWorkflow(true)); + return $list; } @@ -521,7 +533,7 @@ final class HarbormasterBuildViewController $item = new PHUIStatusItemView(); - if ($build->isStopping()) { + if ($build->isPausing()) { $status_name = pht('Pausing'); $icon = PHUIStatusItemView::ICON_RIGHT; $color = 'dark'; diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php index 296e6a9b43..2716befd00 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php @@ -39,8 +39,8 @@ final class HarbormasterBuildableActionController $issuable[] = $build; } break; - case HarbormasterBuildCommand::COMMAND_STOP: - if ($build->canStopBuild()) { + case HarbormasterBuildCommand::COMMAND_PAUSE: + if ($build->canPauseBuild()) { $issuable[] = $build; } break; @@ -49,6 +49,11 @@ final class HarbormasterBuildableActionController $issuable[] = $build; } break; + case HarbormasterBuildCommand::COMMAND_ABORT: + if ($build->canAbortBuild()) { + $issuable[] = $build; + } + break; default: return new Aphront400Response(); } @@ -94,20 +99,32 @@ final class HarbormasterBuildableActionController 'restart all builds?'); $submit = pht('Restart All Builds'); } else { - $title = pht('Unable to Restart Build'); + $title = pht('Unable to Restart Builds'); $body = pht('No builds can be restarted.'); } break; - case HarbormasterBuildCommand::COMMAND_STOP: + case HarbormasterBuildCommand::COMMAND_PAUSE: if ($issuable) { - $title = pht('Really stop all builds?'); + $title = pht('Really pause all builds?'); $body = pht( - 'If you stop all build, work will halt once the current steps '. + 'If you pause all builds, work will halt once the current steps '. 'complete. You can resume the builds later.'); - $submit = pht('Stop All Builds'); + $submit = pht('Pause All Builds'); } else { - $title = pht('Unable to Stop Build'); - $body = pht('No builds can be stopped.'); + $title = pht('Unable to Pause Builds'); + $body = pht('No builds can be paused.'); + } + break; + case HarbormasterBuildCommand::COMMAND_ABORT: + if ($issuable) { + $title = pht('Really abort all builds?'); + $body = pht( + 'If you abort all builds, work will halt immediately. Work '. + 'will be discarded, and builds must be completely restarted.'); + $submit = pht('Abort All Builds'); + } else { + $title = pht('Unable to Abort Builds'); + $body = pht('No builds can be aborted.'); } break; case HarbormasterBuildCommand::COMMAND_RESUME: @@ -116,7 +133,7 @@ final class HarbormasterBuildableActionController $body = pht('Work will continue on all builds. Really resume?'); $submit = pht('Resume All Builds'); } else { - $title = pht('Unable to Resume Build'); + $title = pht('Unable to Resume Builds'); $body = pht('No builds can be resumed.'); } break; diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php index ac55bf40b0..e57c322be4 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php @@ -84,7 +84,8 @@ final class HarbormasterBuildableViewController $can_restart = false; $can_resume = false; - $can_stop = false; + $can_pause = false; + $can_abort = false; foreach ($buildable->getBuilds() as $build) { if ($build->canRestartBuild()) { @@ -93,14 +94,18 @@ final class HarbormasterBuildableViewController if ($build->canResumeBuild()) { $can_resume = true; } - if ($build->canStopBuild()) { - $can_stop = true; + if ($build->canPauseBuild()) { + $can_pause = true; + } + if ($build->canAbortBuild()) { + $can_abort = true; } } $restart_uri = "buildable/{$id}/restart/"; - $stop_uri = "buildable/{$id}/stop/"; + $pause_uri = "buildable/{$id}/pause/"; $resume_uri = "buildable/{$id}/resume/"; + $abort_uri = "buildable/{$id}/abort/"; $list->addAction( id(new PhabricatorActionView()) @@ -114,9 +119,9 @@ final class HarbormasterBuildableViewController id(new PhabricatorActionView()) ->setIcon('fa-pause') ->setName(pht('Pause All Builds')) - ->setHref($this->getApplicationURI($stop_uri)) + ->setHref($this->getApplicationURI($pause_uri)) ->setWorkflow(true) - ->setDisabled(!$can_stop || !$can_edit)); + ->setDisabled(!$can_pause || !$can_edit)); $list->addAction( id(new PhabricatorActionView()) @@ -126,6 +131,14 @@ final class HarbormasterBuildableViewController ->setWorkflow(true) ->setDisabled(!$can_resume || !$can_edit)); + $list->addAction( + id(new PhabricatorActionView()) + ->setIcon('fa-exclamation-triangle') + ->setName(pht('Abort All Builds')) + ->setHref($this->getApplicationURI($abort_uri)) + ->setWorkflow(true) + ->setDisabled(!$can_abort || !$can_edit)); + return $list; } @@ -181,7 +194,7 @@ final class HarbormasterBuildableViewController if ($build->isRestarting()) { $item->addIcon('fa-repeat', pht('Restarting')); - } else if ($build->isStopping()) { + } else if ($build->isPausing()) { $item->addIcon('fa-pause', pht('Pausing')); } else if ($build->isResuming()) { $item->addIcon('fa-play', pht('Resuming')); @@ -191,7 +204,8 @@ final class HarbormasterBuildableViewController $restart_uri = "build/restart/{$build_id}/buildable/"; $resume_uri = "build/resume/{$build_id}/buildable/"; - $stop_uri = "build/stop/{$build_id}/buildable/"; + $pause_uri = "build/pause/{$build_id}/buildable/"; + $abort_uri = "build/abort/{$build_id}/buildable/"; $item->addAction( id(new PHUIListItemView()) @@ -213,9 +227,9 @@ final class HarbormasterBuildableViewController id(new PHUIListItemView()) ->setIcon('fa-pause') ->setName(pht('Pause')) - ->setHref($this->getApplicationURI($stop_uri)) + ->setHref($this->getApplicationURI($pause_uri)) ->setWorkflow(true) - ->setDisabled(!$build->canStopBuild())); + ->setDisabled(!$build->canPauseBuild())); } $targets = $build->getBuildTargets(); diff --git a/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php b/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php index 6c59622f90..b8c39146cb 100644 --- a/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php +++ b/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php @@ -71,12 +71,15 @@ final class HarbormasterBuildTransactionEditor case HarbormasterBuildCommand::COMMAND_RESTART: $issuable = $build->canRestartBuild(); break; - case HarbormasterBuildCommand::COMMAND_STOP: - $issuable = $build->canStopBuild(); + case HarbormasterBuildCommand::COMMAND_PAUSE: + $issuable = $build->canPauseBuild(); break; case HarbormasterBuildCommand::COMMAND_RESUME: $issuable = $build->canResumeBuild(); break; + case HarbormasterBuildCommand::COMMAND_ABORT: + $issuable = $build->canAbortBuild(); + break; default: throw new Exception(pht('Unknown command %s', $command)); } diff --git a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php index f2ed44bb0d..36ca48b060 100644 --- a/src/applications/harbormaster/engine/HarbormasterBuildEngine.php +++ b/src/applications/harbormaster/engine/HarbormasterBuildEngine.php @@ -98,6 +98,12 @@ final class HarbormasterBuildEngine extends Phobject { } private function updateBuild(HarbormasterBuild $build) { + if ($build->isAborting()) { + $this->releaseAllArtifacts($build); + $build->setBuildStatus(HarbormasterBuild::STATUS_ABORTED); + $build->save(); + } + if (($build->getBuildStatus() == HarbormasterBuild::STATUS_PENDING) || ($build->isRestarting())) { $this->restartBuild($build); @@ -110,8 +116,8 @@ final class HarbormasterBuildEngine extends Phobject { $build->save(); } - if ($build->isStopping() && !$build->isComplete()) { - $build->setBuildStatus(HarbormasterBuild::STATUS_STOPPED); + if ($build->isPausing() && !$build->isComplete()) { + $build->setBuildStatus(HarbormasterBuild::STATUS_PAUSED); $build->save(); } diff --git a/src/applications/harbormaster/future/HarbormasterExecFuture.php b/src/applications/harbormaster/future/HarbormasterExecFuture.php new file mode 100644 index 0000000000..9dc4e172fd --- /dev/null +++ b/src/applications/harbormaster/future/HarbormasterExecFuture.php @@ -0,0 +1,50 @@ +future = $future; + return $this; + } + + public function getFuture() { + return $this->future; + } + + public function setLogs( + HarbormasterBuildLog $stdout, + HarbormasterBuildLog $stderr) { + $this->stdout = $stdout; + $this->stderr = $stderr; + return $this; + } + + public function isReady() { + $future = $this->getFuture(); + + $result = $future->isReady(); + + list($stdout, $stderr) = $future->read(); + $future->discardBuffers(); + + if ($this->stdout) { + $this->stdout->append($stdout); + } + + if ($this->stderr) { + $this->stderr->append($stderr); + } + + return $result; + } + + protected function getResult() { + return $this->getFuture()->getResult(); + } + +} diff --git a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php index 15f2dd9943..744ad2474f 100644 --- a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php @@ -238,22 +238,21 @@ abstract class HarbormasterBuildStepImplementation extends Phobject { return $build->getBuildGeneration() !== $target->getBuildGeneration(); } - protected function resolveFuture( + protected function resolveFutures( HarbormasterBuild $build, HarbormasterBuildTarget $target, - Future $future) { + array $futures) { - $futures = new FutureIterator(array($future)); + $futures = new FutureIterator($futures); foreach ($futures->setUpdateInterval(5) as $key => $future) { if ($future === null) { $build->reload(); if ($this->shouldAbort($build, $target)) { throw new HarbormasterBuildAbortedException(); } - } else { - return $future->resolve(); } } + } diff --git a/src/applications/harbormaster/step/HarbormasterDrydockCommandBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterDrydockCommandBuildStepImplementation.php new file mode 100644 index 0000000000..0b19704197 --- /dev/null +++ b/src/applications/harbormaster/step/HarbormasterDrydockCommandBuildStepImplementation.php @@ -0,0 +1,90 @@ +formatSettingForDescription('command'), + $this->formatSettingForDescription('artifact')); + } + + public function execute( + HarbormasterBuild $build, + HarbormasterBuildTarget $build_target) { + $viewer = PhabricatorUser::getOmnipotentUser(); + + $settings = $this->getSettings(); + $variables = $build_target->getVariables(); + + $artifact = $build_target->loadArtifact($settings['artifact']); + $impl = $artifact->getArtifactImplementation(); + $lease = $impl->loadArtifactLease($viewer); + + // TODO: Require active lease. + + $command = $this->mergeVariables( + 'vcsprintf', + $settings['command'], + $variables); + + $interface = $lease->getInterface(DrydockCommandInterface::INTERFACE_TYPE); + + $exec_future = $interface->getExecFuture('%C', $command); + + $harbor_future = id(new HarbormasterExecFuture()) + ->setFuture($exec_future) + ->setLogs( + $build_target->newLog('remote', 'stdout'), + $build_target->newLog('remote', 'stderr')); + + $this->resolveFutures( + $build, + $build_target, + array($harbor_future)); + + list($err) = $harbor_future->resolve(); + if ($err) { + throw new HarbormasterBuildFailureException(); + } + } + + public function getArtifactInputs() { + return array( + array( + 'name' => pht('Drydock Lease'), + 'key' => $this->getSetting('artifact'), + 'type' => HarbormasterWorkingCopyArtifact::ARTIFACTCONST, + ), + ); + } + + public function getFieldSpecifications() { + return array( + 'command' => array( + 'name' => pht('Command'), + 'type' => 'text', + 'required' => true, + ), + 'artifact' => array( + 'name' => pht('Drydock Lease'), + 'type' => 'text', + 'required' => true, + ), + ); + } + +} diff --git a/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php index 3910ac0b24..bfaa5c6a56 100644 --- a/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php @@ -51,9 +51,6 @@ final class HarbormasterHTTPRequestBuildStepImplementation $settings['uri'], $variables); - $log_body = $build->createLog($build_target, $uri, 'http-body'); - $start = $log_body->start(); - $method = nonempty(idx($settings, 'method'), 'POST'); $future = id(new HTTPSFuture($uri)) @@ -70,16 +67,30 @@ final class HarbormasterHTTPRequestBuildStepImplementation $key->getPasswordEnvelope()); } - list($status, $body, $headers) = $this->resolveFuture( + $this->resolveFutures( $build, $build_target, - $future); + array($future)); - $log_body->append($body); - $log_body->finalize($start); + list($status, $body, $headers) = $future->resolve(); + + $header_lines = array(); + foreach ($headers as $header) { + list($head, $tail) = $header; + $header_lines[] = "{$head}: {$tail}"; + } + $header_lines = implode("\n", $header_lines); + + $build_target + ->newLog($uri, 'http.head') + ->append($header_lines); + + $build_target + ->newLog($uri, 'http.body') + ->append($body); if ($status->getStatusCode() != 200) { - $build->setBuildStatus(HarbormasterBuild::STATUS_FAILED); + throw new HarbormasterBuildFailureException(); } } diff --git a/src/applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php new file mode 100644 index 0000000000..1f5b139008 --- /dev/null +++ b/src/applications/harbormaster/step/HarbormasterLeaseWorkingCopyBuildStepImplementation.php @@ -0,0 +1,106 @@ +getSettings(); + + // TODO: We should probably have a separate temporary storage area for + // execution stuff that doesn't step on configuration state? + $lease_phid = $build_target->getDetail('exec.leasePHID'); + + if ($lease_phid) { + $lease = id(new DrydockLeaseQuery()) + ->setViewer($viewer) + ->withPHIDs(array($lease_phid)) + ->executeOne(); + if (!$lease) { + throw new PhabricatorWorkerPermanentFailureException( + pht( + 'Lease "%s" could not be loaded.', + $lease_phid)); + } + } else { + $working_copy_type = id(new DrydockWorkingCopyBlueprintImplementation()) + ->getType(); + + $lease = id(new DrydockLease()) + ->setResourceType($working_copy_type) + ->setOwnerPHID($build_target->getPHID()); + + $variables = $build_target->getVariables(); + + $repository_phid = idx($variables, 'repository.phid'); + $commit = idx($variables, 'repository.commit'); + + $lease + ->setAttribute('repositoryPHID', $repository_phid) + ->setAttribute('commit', $commit); + + $lease->queueForActivation(); + + $build_target + ->setDetail('exec.leasePHID', $lease->getPHID()) + ->save(); + } + + if ($lease->isActivating()) { + // TODO: Smart backoff? + throw new PhabricatorWorkerYieldException(15); + } + + if (!$lease->isActive()) { + // TODO: We could just forget about this lease and retry? + throw new PhabricatorWorkerPermanentFailureException( + pht( + 'Lease "%s" never activated.', + $lease->getPHID())); + } + + $artifact = $build_target->createArtifact( + $viewer, + $settings['name'], + HarbormasterWorkingCopyArtifact::ARTIFACTCONST, + array( + 'drydockLeasePHID' => $lease->getPHID(), + )); + } + + public function getArtifactOutputs() { + return array( + array( + 'name' => pht('Working Copy'), + 'key' => $this->getSetting('name'), + 'type' => HarbormasterWorkingCopyArtifact::ARTIFACTCONST, + ), + ); + } + + public function getFieldSpecifications() { + return array( + 'name' => array( + 'name' => pht('Artifact Name'), + 'type' => 'text', + 'required' => true, + ), + ); + } + +} diff --git a/src/applications/harbormaster/storage/HarbormasterBuildCommand.php b/src/applications/harbormaster/storage/HarbormasterBuildCommand.php index 1522a054ac..50a40d8e98 100644 --- a/src/applications/harbormaster/storage/HarbormasterBuildCommand.php +++ b/src/applications/harbormaster/storage/HarbormasterBuildCommand.php @@ -2,9 +2,10 @@ final class HarbormasterBuildCommand extends HarbormasterDAO { - const COMMAND_STOP = 'stop'; + const COMMAND_PAUSE = 'pause'; const COMMAND_RESUME = 'resume'; const COMMAND_RESTART = 'restart'; + const COMMAND_ABORT = 'abort'; protected $authorPHID; protected $targetPHID; diff --git a/src/applications/harbormaster/storage/HarbormasterBuildTransaction.php b/src/applications/harbormaster/storage/HarbormasterBuildTransaction.php index 07d81b40cb..e16a51008c 100644 --- a/src/applications/harbormaster/storage/HarbormasterBuildTransaction.php +++ b/src/applications/harbormaster/storage/HarbormasterBuildTransaction.php @@ -31,13 +31,17 @@ final class HarbormasterBuildTransaction return pht( '%s restarted this build.', $this->renderHandleLink($author_phid)); + case HarbormasterBuildCommand::COMMAND_ABORT: + return pht( + '%s aborted this build.', + $this->renderHandleLink($author_phid)); case HarbormasterBuildCommand::COMMAND_RESUME: return pht( '%s resumed this build.', $this->renderHandleLink($author_phid)); - case HarbormasterBuildCommand::COMMAND_STOP: + case HarbormasterBuildCommand::COMMAND_PAUSE: return pht( - '%s stopped this build.', + '%s paused this build.', $this->renderHandleLink($author_phid)); } } @@ -59,8 +63,10 @@ final class HarbormasterBuildTransaction return 'fa-backward'; case HarbormasterBuildCommand::COMMAND_RESUME: return 'fa-play'; - case HarbormasterBuildCommand::COMMAND_STOP: - return 'fa-stop'; + case HarbormasterBuildCommand::COMMAND_PAUSE: + return 'fa-pause'; + case HarbormasterBuildCommand::COMMAND_ABORT: + return 'fa-exclamation-triangle'; } } @@ -78,7 +84,8 @@ final class HarbormasterBuildTransaction return 'green'; case self::TYPE_COMMAND: switch ($new) { - case HarbormasterBuildCommand::COMMAND_STOP: + case HarbormasterBuildCommand::COMMAND_PAUSE: + case HarbormasterBuildCommand::COMMAND_ABORT: return 'red'; } } diff --git a/src/applications/harbormaster/storage/HarbormasterBuildableTransaction.php b/src/applications/harbormaster/storage/HarbormasterBuildableTransaction.php index 0696edbc1a..90a26d50c2 100644 --- a/src/applications/harbormaster/storage/HarbormasterBuildableTransaction.php +++ b/src/applications/harbormaster/storage/HarbormasterBuildableTransaction.php @@ -35,9 +35,9 @@ final class HarbormasterBuildableTransaction return pht( '%s resumed this buildable.', $this->renderHandleLink($author_phid)); - case HarbormasterBuildCommand::COMMAND_STOP: + case HarbormasterBuildCommand::COMMAND_PAUSE: return pht( - '%s stopped this buildable.', + '%s paused this buildable.', $this->renderHandleLink($author_phid)); } } @@ -59,8 +59,8 @@ final class HarbormasterBuildableTransaction return 'fa-backward'; case HarbormasterBuildCommand::COMMAND_RESUME: return 'fa-play'; - case HarbormasterBuildCommand::COMMAND_STOP: - return 'fa-stop'; + case HarbormasterBuildCommand::COMMAND_PAUSE: + return 'fa-pause'; } } @@ -78,7 +78,7 @@ final class HarbormasterBuildableTransaction return 'green'; case self::TYPE_COMMAND: switch ($new) { - case HarbormasterBuildCommand::COMMAND_STOP: + case HarbormasterBuildCommand::COMMAND_PAUSE: return 'red'; } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index e35744ecc9..1c012b1842 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -41,15 +41,20 @@ final class HarbormasterBuild extends HarbormasterDAO */ const STATUS_FAILED = 'failed'; + /** + * The build has aborted. + */ + const STATUS_ABORTED = 'aborted'; + /** * The build encountered an unexpected error. */ const STATUS_ERROR = 'error'; /** - * The build has been stopped. + * The build has been paused. */ - const STATUS_STOPPED = 'stopped'; + const STATUS_PAUSED = 'paused'; /** * The build has been deadlocked. @@ -75,9 +80,11 @@ final class HarbormasterBuild extends HarbormasterDAO return pht('Passed'); case self::STATUS_FAILED: return pht('Failed'); + case self::STATUS_ABORTED: + return pht('Aborted'); case self::STATUS_ERROR: return pht('Unexpected Error'); - case self::STATUS_STOPPED: + case self::STATUS_PAUSED: return pht('Paused'); case self::STATUS_DEADLOCKED: return pht('Deadlocked'); @@ -97,9 +104,11 @@ final class HarbormasterBuild extends HarbormasterDAO return PHUIStatusItemView::ICON_ACCEPT; case self::STATUS_FAILED: return PHUIStatusItemView::ICON_REJECT; + case self::STATUS_ABORTED: + return PHUIStatusItemView::ICON_MINUS; case self::STATUS_ERROR: return PHUIStatusItemView::ICON_MINUS; - case self::STATUS_STOPPED: + case self::STATUS_PAUSED: return PHUIStatusItemView::ICON_MINUS; case self::STATUS_DEADLOCKED: return PHUIStatusItemView::ICON_WARNING; @@ -118,10 +127,11 @@ final class HarbormasterBuild extends HarbormasterDAO case self::STATUS_PASSED: return 'green'; case self::STATUS_FAILED: + case self::STATUS_ABORTED: case self::STATUS_ERROR: case self::STATUS_DEADLOCKED: return 'red'; - case self::STATUS_STOPPED: + case self::STATUS_PAUSED: return 'dark'; default: return 'bluegrey'; @@ -241,6 +251,7 @@ final class HarbormasterBuild extends HarbormasterDAO 'buildable.revision' => null, 'buildable.commit' => null, 'repository.callsign' => null, + 'repository.phid' => null, 'repository.vcs' => null, 'repository.uri' => null, 'step.timestamp' => null, @@ -284,16 +295,17 @@ final class HarbormasterBuild extends HarbormasterDAO switch ($this->getBuildStatus()) { case self::STATUS_PASSED: case self::STATUS_FAILED: + case self::STATUS_ABORTED: case self::STATUS_ERROR: - case self::STATUS_STOPPED: + case self::STATUS_PAUSED: return true; } return false; } - public function isStopped() { - return ($this->getBuildStatus() == self::STATUS_STOPPED); + public function isPaused() { + return ($this->getBuildStatus() == self::STATUS_PAUSED); } @@ -317,14 +329,22 @@ final class HarbormasterBuild extends HarbormasterDAO return !$this->isRestarting(); } - public function canStopBuild() { + public function canPauseBuild() { if ($this->isAutobuild()) { return false; } return !$this->isComplete() && - !$this->isStopped() && - !$this->isStopping(); + !$this->isPaused() && + !$this->isPausing(); + } + + public function canAbortBuild() { + if ($this->isAutobuild()) { + return false; + } + + return !$this->isComplete(); } public function canResumeBuild() { @@ -332,26 +352,29 @@ final class HarbormasterBuild extends HarbormasterDAO return false; } - return $this->isStopped() && + return $this->isPaused() && !$this->isResuming(); } - public function isStopping() { - $is_stopping = false; + public function isPausing() { + $is_pausing = false; foreach ($this->getUnprocessedCommands() as $command_object) { $command = $command_object->getCommand(); switch ($command) { - case HarbormasterBuildCommand::COMMAND_STOP: - $is_stopping = true; + case HarbormasterBuildCommand::COMMAND_PAUSE: + $is_pausing = true; break; case HarbormasterBuildCommand::COMMAND_RESUME: case HarbormasterBuildCommand::COMMAND_RESTART: - $is_stopping = false; + $is_pausing = false; + break; + case HarbormasterBuildCommand::COMMAND_ABORT: + $is_pausing = true; break; } } - return $is_stopping; + return $is_pausing; } public function isResuming() { @@ -363,7 +386,10 @@ final class HarbormasterBuild extends HarbormasterDAO case HarbormasterBuildCommand::COMMAND_RESUME: $is_resuming = true; break; - case HarbormasterBuildCommand::COMMAND_STOP: + case HarbormasterBuildCommand::COMMAND_PAUSE: + $is_resuming = false; + break; + case HarbormasterBuildCommand::COMMAND_ABORT: $is_resuming = false; break; } @@ -386,6 +412,20 @@ final class HarbormasterBuild extends HarbormasterDAO return $is_restarting; } + public function isAborting() { + $is_aborting = false; + foreach ($this->getUnprocessedCommands() as $command_object) { + $command = $command_object->getCommand(); + switch ($command) { + case HarbormasterBuildCommand::COMMAND_ABORT: + $is_aborting = true; + break; + } + } + + return $is_aborting; + } + public function deleteUnprocessedCommands() { foreach ($this->getUnprocessedCommands() as $key => $command_object) { $command_object->delete(); diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildLintMessage.php b/src/applications/harbormaster/storage/build/HarbormasterBuildLintMessage.php index 2bd37b2f79..4fe230d452 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildLintMessage.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildLintMessage.php @@ -104,7 +104,7 @@ final class HarbormasterBuildLintMessage 'path' => 'text', 'line' => 'uint32?', 'characterOffset' => 'uint32?', - 'code' => 'text32', + 'code' => 'text128', 'severity' => 'text32', 'name' => 'text255', ), diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php index d03f588719..2e964e8931 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildLog.php @@ -10,6 +10,7 @@ final class HarbormasterBuildLog extends HarbormasterDAO protected $live; private $buildTarget = self::ATTACHABLE; + private $start; const CHUNK_BYTE_LIMIT = 102400; @@ -18,6 +19,12 @@ final class HarbormasterBuildLog extends HarbormasterDAO */ const ENCODING_TEXT = 'text'; + public function __destruct() { + if ($this->live) { + $this->finalize($this->start); + } + } + public static function initializeNewBuildLog( HarbormasterBuildTarget $build_target) { @@ -75,6 +82,8 @@ final class HarbormasterBuildLog extends HarbormasterDAO $this->setLive(1); $this->save(); + $this->start = PhabricatorTime::getNow(); + return time(); } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php b/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php index 7f08292787..fffa30a883 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php @@ -249,6 +249,20 @@ final class HarbormasterBuildTarget extends HarbormasterDAO return $artifact; } + public function newLog($log_source, $log_type) { + $log_source = id(new PhutilUTF8StringTruncator()) + ->setMaximumBytes(250) + ->truncateString($log_source); + + $log = HarbormasterBuildLog::initializeNewBuildLog($this) + ->setLogSource($log_source) + ->setLogType($log_type); + + $log->start(); + + return $log; + } + /* -( Status )------------------------------------------------------------- */ @@ -268,6 +282,7 @@ final class HarbormasterBuildTarget extends HarbormasterDAO public function isFailed() { switch ($this->getTargetStatus()) { case self::STATUS_FAILED: + case self::STATUS_ABORTED: return true; } diff --git a/src/applications/herald/controller/HeraldRuleController.php b/src/applications/herald/controller/HeraldRuleController.php index ce7a46cd00..37a583c7ef 100644 --- a/src/applications/herald/controller/HeraldRuleController.php +++ b/src/applications/herald/controller/HeraldRuleController.php @@ -24,6 +24,8 @@ final class HeraldRuleController extends HeraldController { } $cancel_uri = $this->getApplicationURI("rule/{$id}/"); } else { + $new_uri = $this->getApplicationURI('new/'); + $rule = new HeraldRule(); $rule->setAuthorPHID($viewer->getPHID()); $rule->setMustMatchAll(1); @@ -33,18 +35,40 @@ final class HeraldRuleController extends HeraldController { $rule_type = $request->getStr('rule_type'); if (!isset($rule_type_map[$rule_type])) { - $rule_type = HeraldRuleTypeConfig::RULE_TYPE_PERSONAL; + return $this->newDialog() + ->setTitle(pht('Invalid Rule Type')) + ->appendParagraph( + pht( + 'The selected rule type ("%s") is not recognized by Herald.', + $rule_type)) + ->addCancelButton($new_uri); } $rule->setRuleType($rule_type); - $adapter = HeraldAdapter::getAdapterForContentType( - $rule->getContentType()); + try { + $adapter = HeraldAdapter::getAdapterForContentType( + $rule->getContentType()); + } catch (Exception $ex) { + return $this->newDialog() + ->setTitle(pht('Invalid Content Type')) + ->appendParagraph( + pht( + 'The selected content type ("%s") is not recognized by '. + 'Herald.', + $rule->getContentType())) + ->addCancelButton($new_uri); + } if (!$adapter->supportsRuleType($rule->getRuleType())) { - throw new Exception( - pht( - "This rule's content type does not support the selected rule ". - "type.")); + return $this->newDialog() + ->setTitle(pht('Rule/Content Mismatch')) + ->appendParagraph( + pht( + 'The selected rule type ("%s") is not supported by the selected '. + 'content type ("%s").', + $rule->getRuleType(), + $rule->getContentType())) + ->addCancelButton($new_uri); } if ($rule->isObjectRule()) { diff --git a/src/applications/legalpad/controller/LegalpadDocumentManageController.php b/src/applications/legalpad/controller/LegalpadDocumentManageController.php index 9fc773258f..1ca838258b 100644 --- a/src/applications/legalpad/controller/LegalpadDocumentManageController.php +++ b/src/applications/legalpad/controller/LegalpadDocumentManageController.php @@ -84,7 +84,8 @@ final class LegalpadDocumentManageController extends LegalpadController { $view = new PHUIPropertyListView(); $view->addClass('legalpad'); - $view->addSectionHeader(pht('Document')); + $view->addSectionHeader( + pht('Document'), 'fa-file-text-o'); $view->addTextContent( $engine->getOutput($body, LegalpadDocumentBody::MARKUP_FIELD_TEXT)); diff --git a/src/applications/meta/controller/PhabricatorApplicationDetailViewController.php b/src/applications/meta/controller/PhabricatorApplicationDetailViewController.php index da23cc31ff..702d0b2130 100644 --- a/src/applications/meta/controller/PhabricatorApplicationDetailViewController.php +++ b/src/applications/meta/controller/PhabricatorApplicationDetailViewController.php @@ -106,8 +106,7 @@ final class PhabricatorApplicationDetailViewController $overview = $application->getOverview(); if ($overview) { $properties->addSectionHeader( - pht('Overview'), - PHUIPropertyListView::ICON_SUMMARY); + pht('Overview'), PHUIPropertyListView::ICON_SUMMARY); $properties->addTextContent( PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent($overview), @@ -119,7 +118,8 @@ final class PhabricatorApplicationDetailViewController $viewer, $application); - $properties->addSectionHeader(pht('Policies')); + $properties->addSectionHeader( + pht('Policies'), 'fa-lock'); foreach ($application->getCapabilities() as $capability) { $properties->addProperty( diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php b/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php index 7cb1ad71d6..ec43312e23 100644 --- a/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php +++ b/src/applications/metamta/controller/PhabricatorMetaMTAMailViewController.php @@ -218,7 +218,8 @@ final class PhabricatorMetaMTAMailViewController 'Delivery reasons are listed from weakest to strongest.'))); } - $properties->addSectionHeader(pht('Routing Rules')); + $properties->addSectionHeader( + pht('Routing Rules'), 'fa-paper-plane-o'); $map = $mail->getDeliveredRoutingMap(); $routing_detail = null; diff --git a/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php b/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php index 607b0eda6c..675b2634a9 100644 --- a/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php +++ b/src/applications/nuance/source/NuancePhabricatorFormSourceDefinition.php @@ -125,7 +125,8 @@ final class NuancePhabricatorFormSourceDefinition 'default', $viewer); - $view->addSectionHeader(pht('Complaint')); + $view->addSectionHeader( + pht('Complaint'), 'fa-exclamation-circle'); $view->addTextContent($complaint); } diff --git a/src/applications/owners/controller/PhabricatorOwnersDetailController.php b/src/applications/owners/controller/PhabricatorOwnersDetailController.php index 7edcf010b6..bb178cfbcb 100644 --- a/src/applications/owners/controller/PhabricatorOwnersDetailController.php +++ b/src/applications/owners/controller/PhabricatorOwnersDetailController.php @@ -189,7 +189,8 @@ final class PhabricatorOwnersDetailController $description = $package->getDescription(); if (strlen($description)) { - $view->addSectionHeader(pht('Description')); + $view->addSectionHeader( + pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $view->addTextContent( $output = PhabricatorMarkupEngine::renderOneObject( id(new PhabricatorMarkupOneOff())->setContent($description), diff --git a/src/applications/passphrase/controller/PassphraseCredentialEditController.php b/src/applications/passphrase/controller/PassphraseCredentialEditController.php index 8a2a8da70f..89fe783fac 100644 --- a/src/applications/passphrase/controller/PassphraseCredentialEditController.php +++ b/src/applications/passphrase/controller/PassphraseCredentialEditController.php @@ -31,7 +31,7 @@ final class PassphraseCredentialEditController extends PassphraseController { throw new Exception( pht( 'Credential has noncreateable type "%s"!', - $credential->getCredentialType())); + $type_const)); } $credential = PassphraseCredential::initializeNewCredential($viewer) diff --git a/src/applications/paste/application/PhabricatorPasteApplication.php b/src/applications/paste/application/PhabricatorPasteApplication.php index a3f11bb9a8..7fe3ec41e7 100644 --- a/src/applications/paste/application/PhabricatorPasteApplication.php +++ b/src/applications/paste/application/PhabricatorPasteApplication.php @@ -40,6 +40,7 @@ final class PhabricatorPasteApplication extends PhabricatorApplication { '(query/(?P[^/]+)/)?' => 'PhabricatorPasteListController', 'create/' => 'PhabricatorPasteEditController', 'edit/(?P[1-9]\d*)/' => 'PhabricatorPasteEditController', + 'raw/(?P[1-9]\d*)/' => 'PhabricatorPasteRawController', 'comment/(?P[1-9]\d*)/' => 'PhabricatorPasteCommentController', ), ); diff --git a/src/applications/paste/controller/PhabricatorPasteRawController.php b/src/applications/paste/controller/PhabricatorPasteRawController.php new file mode 100644 index 0000000000..080c5dc8b5 --- /dev/null +++ b/src/applications/paste/controller/PhabricatorPasteRawController.php @@ -0,0 +1,39 @@ +getViewer(); + $id = $request->getURIData('id'); + + $paste = id(new PhabricatorPasteQuery()) + ->setViewer($viewer) + ->withIDs(array($id)) + ->executeOne(); + if (!$paste) { + return new Aphront404Response(); + } + + $file = id(new PhabricatorFileQuery()) + ->setViewer($viewer) + ->withPHIDs(array($paste->getFilePHID())) + ->executeOne(); + if (!$file) { + return new Aphront400Response(); + } + + return $file->getRedirectResponse(); + } + +} diff --git a/src/applications/paste/controller/PhabricatorPasteViewController.php b/src/applications/paste/controller/PhabricatorPasteViewController.php index 382d683f37..f8176892bc 100644 --- a/src/applications/paste/controller/PhabricatorPasteViewController.php +++ b/src/applications/paste/controller/PhabricatorPasteViewController.php @@ -42,14 +42,6 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { return new Aphront404Response(); } - $file = id(new PhabricatorFileQuery()) - ->setViewer($viewer) - ->withPHIDs(array($paste->getFilePHID())) - ->executeOne(); - if (!$file) { - return new Aphront400Response(); - } - $forks = id(new PhabricatorPasteQuery()) ->setViewer($viewer) ->withParentPHIDs(array($paste->getPHID())) @@ -57,7 +49,7 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { $fork_phids = mpull($forks, 'getPHID'); $header = $this->buildHeaderView($paste); - $actions = $this->buildActionView($viewer, $paste, $file); + $actions = $this->buildActionView($viewer, $paste); $properties = $this->buildPropertyView($paste, $fork_phids, $actions); $object_box = id(new PHUIObjectBoxView()) @@ -139,8 +131,7 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { private function buildActionView( PhabricatorUser $viewer, - PhabricatorPaste $paste, - PhabricatorFile $file) { + PhabricatorPaste $paste) { $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, @@ -148,7 +139,8 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { PhabricatorPolicyCapability::CAN_EDIT); $can_fork = $viewer->isLoggedIn(); - $fork_uri = $this->getApplicationURI('/create/?parent='.$paste->getID()); + $id = $paste->getID(); + $fork_uri = $this->getApplicationURI('/create/?parent='.$id); return id(new PhabricatorActionListView()) ->setUser($viewer) @@ -160,7 +152,7 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { ->setIcon('fa-pencil') ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit) - ->setHref($this->getApplicationURI('/edit/'.$paste->getID().'/'))) + ->setHref($this->getApplicationURI("edit/{$id}/"))) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Fork This Paste')) @@ -172,7 +164,7 @@ final class PhabricatorPasteViewController extends PhabricatorPasteController { id(new PhabricatorActionView()) ->setName(pht('View Raw File')) ->setIcon('fa-file-text-o') - ->setHref($file->getBestURI())); + ->setHref($this->getApplicationURI("raw/{$id}/"))); } private function buildPropertyView( diff --git a/src/applications/people/controller/PhabricatorPeopleProfileController.php b/src/applications/people/controller/PhabricatorPeopleProfileController.php index dbdaf7e8b9..30af5057dd 100644 --- a/src/applications/people/controller/PhabricatorPeopleProfileController.php +++ b/src/applications/people/controller/PhabricatorPeopleProfileController.php @@ -136,11 +136,14 @@ final class PhabricatorPeopleProfileController ->setWorkflow(true) ->setHref($this->getApplicationURI('delete/'.$user->getID().'/'))); + $can_welcome = $user->canEstablishWebSessions(); + $actions->addAction( id(new PhabricatorActionView()) ->setIcon('fa-envelope') ->setName(pht('Send Welcome Email')) ->setWorkflow(true) + ->setDisabled(!$can_welcome) ->setHref($this->getApplicationURI('welcome/'.$user->getID().'/'))); } diff --git a/src/applications/people/controller/PhabricatorPeopleWelcomeController.php b/src/applications/people/controller/PhabricatorPeopleWelcomeController.php index b3762e67c9..14b1544b7f 100644 --- a/src/applications/people/controller/PhabricatorPeopleWelcomeController.php +++ b/src/applications/people/controller/PhabricatorPeopleWelcomeController.php @@ -3,19 +3,12 @@ final class PhabricatorPeopleWelcomeController extends PhabricatorPeopleController { - private $id; - - public function willProcessRequest(array $data) { - $this->id = $data['id']; - } - - public function processRequest() { - $request = $this->getRequest(); - $admin = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $admin = $this->getViewer(); $user = id(new PhabricatorPeopleQuery()) ->setViewer($admin) - ->withIDs(array($this->id)) + ->withIDs(array($request->getURIData('id'))) ->executeOne(); if (!$user) { return new Aphront404Response(); @@ -23,6 +16,18 @@ final class PhabricatorPeopleWelcomeController $profile_uri = '/p/'.$user->getUsername().'/'; + if (!$user->canEstablishWebSessions()) { + return $this->newDialog() + ->setTitle(pht('Not a Normal User')) + ->appendParagraph( + pht( + 'You can not send this user a welcome mail because they are not '. + 'a normal user and can not log in to the web interface. Special '. + 'users (like bots and mailing lists) are unable to establish web '. + 'sessions.')) + ->addCancelButton($profile_uri, pht('Done')); + } + if ($request->isFormPost()) { $user->sendWelcomeEmail($admin); return id(new AphrontRedirectResponse())->setURI($profile_uri); diff --git a/src/applications/people/storage/PhabricatorUser.php b/src/applications/people/storage/PhabricatorUser.php index 82d17983e8..77d1d41603 100644 --- a/src/applications/people/storage/PhabricatorUser.php +++ b/src/applications/people/storage/PhabricatorUser.php @@ -587,6 +587,13 @@ final class PhabricatorUser } public function sendWelcomeEmail(PhabricatorUser $admin) { + if (!$this->canEstablishWebSessions()) { + throw new Exception( + pht( + 'Can not send welcome mail to users who can not establish '. + 'web sessions!')); + } + $admin_username = $admin->getUserName(); $admin_realname = $admin->getRealName(); $user_username = $this->getUserName(); diff --git a/src/applications/phame/application/PhabricatorPhameApplication.php b/src/applications/phame/application/PhabricatorPhameApplication.php index f7643c6097..ec98087adf 100644 --- a/src/applications/phame/application/PhabricatorPhameApplication.php +++ b/src/applications/phame/application/PhabricatorPhameApplication.php @@ -42,6 +42,7 @@ final class PhabricatorPhameApplication extends PhabricatorApplication { 'live/(?P[^/]+)/(?P.*)' => 'PhameBlogLiveController', 'post/' => array( '(?:(?Pdraft|all)/)?' => 'PhamePostListController', + '(?:query/(?P[^/]+)/)?' => 'PhamePostListController', 'blogger/(?P[\w\.-_]+)/' => 'PhamePostListController', 'delete/(?P[^/]+)/' => 'PhamePostDeleteController', 'edit/(?:(?P[^/]+)/)?' => 'PhamePostEditController', @@ -56,6 +57,7 @@ final class PhabricatorPhameApplication extends PhabricatorApplication { ), 'blog/' => array( '(?:(?Puser|all)/)?' => 'PhameBlogListController', + '(?:query/(?P[^/]+)/)?' => 'PhameBlogListController', 'delete/(?P[^/]+)/' => 'PhameBlogDeleteController', 'edit/(?P[^/]+)/' => 'PhameBlogEditController', 'view/(?P[^/]+)/' => 'PhameBlogViewController', diff --git a/src/applications/phame/controller/blog/PhameBlogListController.php b/src/applications/phame/controller/blog/PhameBlogListController.php index 9d0253d89c..965e1eec49 100644 --- a/src/applications/phame/controller/blog/PhameBlogListController.php +++ b/src/applications/phame/controller/blog/PhameBlogListController.php @@ -2,86 +2,33 @@ final class PhameBlogListController extends PhameController { - public function handleRequest(AphrontRequest $request) { - $user = $request->getUser(); - - $nav = $this->renderSideNavFilterView(null); - $filter = $request->getURIData('filter'); - $filter = $nav->selectFilter('blog/'.$filter, 'blog/user'); - - $query = id(new PhameBlogQuery()) - ->setViewer($user); - - switch ($filter) { - case 'blog/all': - $title = pht('All Blogs'); - $nodata = pht('No blogs have been created.'); - break; - case 'blog/user': - $title = pht('Joinable Blogs'); - $nodata = pht('There are no blogs you can contribute to.'); - $query->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_JOIN, - )); - break; - default: - throw new Exception(pht("Unknown filter '%s'!", $filter)); - } - - $pager = id(new PHUIPagerView()) - ->setURI($request->getRequestURI(), 'offset') - ->setOffset($request->getInt('offset')); - - $blogs = $query->executeWithOffsetPager($pager); - - $blog_list = $this->renderBlogList($blogs, $user, $nodata); - $blog_list->setPager($pager); - - $box = id(new PHUIObjectBoxView()) - ->setHeaderText($title) - ->setObjectList($blog_list); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb($title, $this->getApplicationURI()); - - $nav->appendChild( - array( - $crumbs, - $box, - )); - - return $this->buildApplicationPage( - $nav, - array( - 'title' => $title, - )); + public function shouldAllowPublic() { + return true; } - private function renderBlogList( - array $blogs, - PhabricatorUser $viewer, - $nodata) { + public function handleRequest(AphrontRequest $request) { + $query_key = $request->getURIData('queryKey'); + $controller = id(new PhabricatorApplicationSearchController()) + ->setQueryKey($query_key) + ->setSearchEngine(new PhameBlogSearchEngine()) + ->setNavigation($this->buildSideNavView()); - $view = new PHUIObjectItemListView(); - $view->setNoDataString($nodata); - $view->setUser($viewer); - foreach ($blogs as $blog) { + return $this->delegateToController($controller); + } - $id = $blog->getID(); - $item = id(new PHUIObjectItemView()) - ->setUser($viewer) - ->setObject($blog) - ->setHeader($blog->getName()) - ->setStatusIcon('fa-star') - ->setHref($this->getApplicationURI("/blog/view/{$id}/")) - ->addAttribute($blog->getSkin()) - ->addAttribute($blog->getDomain()); + public function buildSideNavView() { + $viewer = $this->getRequest()->getUser(); - $view->addItem($item); - } + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); - return $view; + id(new PhameBlogSearchEngine()) + ->setViewer($viewer) + ->addNavigationItems($nav->getMenu()); + + $nav->selectFilter(null); + + return $nav; } } diff --git a/src/applications/phame/controller/post/PhamePostListController.php b/src/applications/phame/controller/post/PhamePostListController.php index b2b28bd4eb..fb38a830bb 100644 --- a/src/applications/phame/controller/post/PhamePostListController.php +++ b/src/applications/phame/controller/post/PhamePostListController.php @@ -2,78 +2,37 @@ final class PhamePostListController extends PhameController { - public function handleRequest(AphrontRequest $request) { - $viewer = $request->getViewer(); - $filter = $request->getURIData('filter'); - $bloggername = $request->getURIData('bloggername'); - - $query = id(new PhamePostQuery()) - ->setViewer($viewer); - - $nav = $this->renderSideNavFilterView(); - $nodata = null; - - switch ($filter) { - case 'draft': - $query->withBloggerPHIDs(array($viewer->getPHID())); - $query->withVisibility(PhamePost::VISIBILITY_DRAFT); - $nodata = pht('You have no unpublished drafts.'); - $title = pht('Unpublished Drafts'); - $nav->selectFilter('post/draft'); - break; - case 'blogger': - if ($bloggername) { - $blogger = id(new PhabricatorUser())->loadOneWhere( - 'username = %s', - $bloggername); - if (!$blogger) { - return new Aphront404Response(); - } - } else { - $blogger = $viewer; - } - - $query->withBloggerPHIDs(array($blogger->getPHID())); - if ($blogger->getPHID() == $viewer->getPHID()) { - $nav->selectFilter('post'); - $nodata = pht('You have not written any posts.'); - } else { - $nodata = pht('%s has not written any posts.', $blogger); - } - $title = pht('Posts by %s', $blogger); - break; - default: - case 'all': - $nodata = pht('There are no visible posts.'); - $title = pht('Posts'); - $nav->selectFilter('post/all'); - break; - } - - $pager = id(new AphrontCursorPagerView()) - ->readFromRequest($request); - - $posts = $query->executeWithCursorPager($pager); - - $post_list = $this->renderPostList($posts, $viewer, $nodata); - $post_list = id(new PHUIObjectBoxView()) - ->setHeaderText($title) - ->appendChild($post_list); - - $crumbs = $this->buildApplicationCrumbs(); - $crumbs->addTextCrumb($title, $this->getApplicationURI()); - - $nav->appendChild( - array( - $crumbs, - $post_list, - )); - - return $this->buildApplicationPage( - $nav, - array( - 'title' => $title, - )); + public function shouldAllowPublic() { + return true; } + public function handleRequest(AphrontRequest $request) { + $query_key = $request->getURIData('queryKey'); + $controller = id(new PhabricatorApplicationSearchController()) + ->setQueryKey($query_key) + ->setSearchEngine(new PhamePostSearchEngine()) + ->setNavigation($this->buildSideNavView()); + + return $this->delegateToController($controller); + } + + public function buildSideNavView() { + $viewer = $this->getRequest()->getUser(); + + $nav = new AphrontSideNavFilterView(); + $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); + + id(new PhamePostSearchEngine()) + ->setViewer($viewer) + ->addNavigationItems($nav->getMenu()); + + $nav->addLabel(pht('Blogs')); + $nav->addFilter('blog/', pht('Manage Blogs')); + + $nav->selectFilter(null); + + return $nav; + } + + } diff --git a/src/applications/phame/query/PhamePostSearchEngine.php b/src/applications/phame/query/PhamePostSearchEngine.php index a5feb3d634..cedc02cc7a 100644 --- a/src/applications/phame/query/PhamePostSearchEngine.php +++ b/src/applications/phame/query/PhamePostSearchEngine.php @@ -29,6 +29,7 @@ final class PhamePostSearchEngine return array( id(new PhabricatorSearchSelectField()) ->setKey('visibility') + ->setLabel(pht('Visibility')) ->setOptions(array( '' => pht('All'), PhamePost::VISIBILITY_PUBLISHED => pht('Live'), diff --git a/src/applications/phortune/controller/PhortuneMerchantViewController.php b/src/applications/phortune/controller/PhortuneMerchantViewController.php index 4b515c44c5..4bedbd9e9c 100644 --- a/src/applications/phortune/controller/PhortuneMerchantViewController.php +++ b/src/applications/phortune/controller/PhortuneMerchantViewController.php @@ -141,7 +141,8 @@ final class PhortuneMerchantViewController 'default', $viewer); - $view->addSectionHeader(pht('Description')); + $view->addSectionHeader( + pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $view->addTextContent($description); } diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommit.php b/src/applications/repository/storage/PhabricatorRepositoryCommit.php index 984e6325f7..531bac9dad 100644 --- a/src/applications/repository/storage/PhabricatorRepositoryCommit.php +++ b/src/applications/repository/storage/PhabricatorRepositoryCommit.php @@ -332,6 +332,7 @@ final class PhabricatorRepositoryCommit $repo = $this->getRepository(); $results['repository.callsign'] = $repo->getCallsign(); + $results['repository.phid'] = $repo->getPHID(); $results['repository.vcs'] = $repo->getVersionControlSystem(); $results['repository.uri'] = $repo->getPublicCloneURI(); @@ -343,6 +344,8 @@ final class PhabricatorRepositoryCommit 'buildable.commit' => pht('The commit identifier, if applicable.'), 'repository.callsign' => pht('The callsign of the repository in Phabricator.'), + 'repository.phid' => + pht('The PHID of the repository in Phabricator.'), 'repository.vcs' => pht('The version control system, either "svn", "hg" or "git".'), 'repository.uri' => diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionDetailController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionDetailController.php index ea4bc5d77b..af6b93bcf1 100644 --- a/src/applications/transactions/controller/PhabricatorApplicationTransactionDetailController.php +++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionDetailController.php @@ -3,22 +3,16 @@ final class PhabricatorApplicationTransactionDetailController extends PhabricatorApplicationTransactionController { - private $phid; - public function shouldAllowPublic() { return true; } - public function willProcessRequest(array $data) { - $this->phid = $data['phid']; - } - - public function processRequest() { - $request = $this->getRequest(); - $viewer = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $viewer = $this->getViewer(); + $phid = $request->getURIData('phid'); $xaction = id(new PhabricatorObjectQuery()) - ->withPHIDs(array($this->phid)) + ->withPHIDs(array($phid)) ->setViewer($viewer) ->executeOne(); if (!$xaction) { @@ -26,17 +20,14 @@ final class PhabricatorApplicationTransactionDetailController } $details = $xaction->renderChangeDetails($viewer); - $cancel_uri = $this->guessCancelURI($viewer, $xaction); - $dialog = id(new AphrontDialogView()) - ->setUser($viewer) + + return $this->newDialog() ->setTitle(pht('Change Details')) ->setWidth(AphrontDialogView::WIDTH_FULL) ->setFlush(true) ->appendChild($details) ->addCancelButton($cancel_uri); - - return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/docs/user/configuration/custom_fields.diviner b/src/docs/user/configuration/custom_fields.diviner index 6643a0518f..9ada0efa06 100644 --- a/src/docs/user/configuration/custom_fields.diviner +++ b/src/docs/user/configuration/custom_fields.diviner @@ -144,6 +144,9 @@ change, but are documented here for completeness: - **Credentials**: Controls with type `credential` allow selection of a Passphrase credential which provides `credential.provides`, and creation of credentials of `credential.type`. + - **Datasource**: Controls with type `datasource` allow selection of tokens + from an arbitrary datasource, controlled with `datasource.class` and + `datasource.parameters`. = Advanced Custom Fields = diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDatasource.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDatasource.php new file mode 100644 index 0000000000..49b9ab2cb5 --- /dev/null +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldDatasource.php @@ -0,0 +1,28 @@ +getFieldConfigValue('datasource.parameters', array()); + + $class = $this->getFieldConfigValue('datasource.class'); + $parent = 'PhabricatorTypeaheadDatasource'; + if (!is_subclass_of($class, $parent)) { + throw new Exception( + pht( + 'Configured datasource class "%s" must be a valid subclass of '. + '"%s".', + $class, + $parent)); + } + + return newv($class, array()) + ->setParameters($parameters); + } + +} diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php index 842d662904..d900d41eb3 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldPHIDs.php @@ -166,4 +166,14 @@ abstract class PhabricatorStandardCustomFieldPHIDs ); } + public function getHeraldFieldValue() { + // If the field has a `null` value, make sure we hand an `array()` to + // Herald. + $value = parent::getHeraldFieldValue(); + if ($value) { + return $value; + } + return array(); + } + } diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php new file mode 100644 index 0000000000..f6a542ec7f --- /dev/null +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldTokenizer.php @@ -0,0 +1,47 @@ +getFieldValue(); + + $control = id(new AphrontFormTokenizerControl()) + ->setUser($this->getViewer()) + ->setLabel($this->getFieldName()) + ->setName($this->getFieldKey()) + ->setDatasource($this->getDatasource()) + ->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->getDatasource()) + ->setValue(nonempty($value, array())); + + $form->appendControl($control); + } + + public function getHeraldFieldValueType($condition) { + return id(new HeraldTokenizerFieldValue()) + ->setKey('custom.'.$this->getFieldKey()) + ->setDatasource($this->getDatasource()); + } + +} diff --git a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php index 7efe873a1d..2f1e6db53a 100644 --- a/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php +++ b/src/infrastructure/customfield/standard/PhabricatorStandardCustomFieldUsers.php @@ -1,49 +1,14 @@ getFieldValue(); - - $control = id(new AphrontFormTokenizerControl()) - ->setUser($this->getViewer()) - ->setLabel($this->getFieldName()) - ->setName($this->getFieldKey()) - ->setDatasource(new PhabricatorPeopleDatasource()) - ->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(new PhabricatorPeopleDatasource()) - ->setValue(nonempty($value, array())); - - $form->appendControl($control); - } - - public function getHeraldFieldValueType($condition) { - return id(new HeraldTokenizerFieldValue()) - ->setKey('custom.'.$this->getFieldKey()) - ->setDatasource(new PhabricatorPeopleDatasource()); + public function getDatasource() { + return new PhabricatorPeopleDatasource(); } } diff --git a/src/infrastructure/daemon/workers/PhabricatorWorker.php b/src/infrastructure/daemon/workers/PhabricatorWorker.php index 629e688b90..4e71440604 100644 --- a/src/infrastructure/daemon/workers/PhabricatorWorker.php +++ b/src/infrastructure/daemon/workers/PhabricatorWorker.php @@ -87,6 +87,15 @@ abstract class PhabricatorWorker extends Phobject { return $this->data; } + final protected function getTaskDataValue($key, $default = null) { + $data = $this->getTaskData(); + if (!is_array($data)) { + throw new PhabricatorWorkerPermanentFailureException( + pht('Expected task data to be a dictionary.')); + } + return idx($data, $key, $default); + } + final public function executeTask() { $this->doWork(); } @@ -108,6 +117,11 @@ abstract class PhabricatorWorker extends Phobject { ->setPriority($priority) ->setObjectPHID($object_phid); + $delay = idx($options, 'delayUntil'); + if ($delay) { + $task->setLeaseExpires($delay); + } + if (self::$runAllTasksInProcess) { // Do the work in-process. $worker = newv($task_class, array($data)); @@ -148,67 +162,6 @@ abstract class PhabricatorWorker extends Phobject { } - /** - * Wait for tasks to complete. If tasks are not leased by other workers, they - * will be executed in this process while waiting. - * - * @param list List of queued task IDs to wait for. - * @return void - */ - final public static function waitForTasks(array $task_ids) { - if (!$task_ids) { - return; - } - - $task_table = new PhabricatorWorkerActiveTask(); - - $waiting = array_fuse($task_ids); - while ($waiting) { - $conn_w = $task_table->establishConnection('w'); - - // Check if any of the tasks we're waiting on are still queued. If they - // are not, we're done waiting. - $row = queryfx_one( - $conn_w, - 'SELECT COUNT(*) N FROM %T WHERE id IN (%Ld)', - $task_table->getTableName(), - $waiting); - if (!$row['N']) { - // Nothing is queued anymore. Stop waiting. - break; - } - - $tasks = id(new PhabricatorWorkerLeaseQuery()) - ->withIDs($waiting) - ->setLimit(1) - ->execute(); - - if (!$tasks) { - // We were not successful in leasing anything. Sleep for a bit and - // see if we have better luck later. - sleep(1); - continue; - } - - $task = head($tasks)->executeTask(); - - $ex = $task->getExecutionException(); - if ($ex) { - throw $ex; - } - } - - $tasks = id(new PhabricatorWorkerArchiveTaskQuery()) - ->withIDs($task_ids) - ->execute(); - - foreach ($tasks as $task) { - if ($task->getResult() != PhabricatorWorkerArchiveTask::RESULT_SUCCESS) { - throw new Exception(pht('Task %d failed!', $task->getID())); - } - } - } - public function renderForDisplay(PhabricatorUser $viewer) { return null; } diff --git a/src/view/phui/PHUIPropertyListView.php b/src/view/phui/PHUIPropertyListView.php index 96c249d866..8a44ecd941 100644 --- a/src/view/phui/PHUIPropertyListView.php +++ b/src/view/phui/PHUIPropertyListView.php @@ -10,8 +10,8 @@ final class PHUIPropertyListView extends AphrontView { private $classes = array(); private $stacked; - const ICON_SUMMARY = 'fa-align-left bluegrey'; - const ICON_TESTPLAN = 'fa-file-text-o bluegrey'; + const ICON_SUMMARY = 'fa-align-left'; + const ICON_TESTPLAN = 'fa-file-text-o'; protected function canAppendChild() { return false; @@ -247,7 +247,7 @@ final class PHUIPropertyListView extends AphrontView { $name = $part['name']; if ($part['icon']) { $icon = id(new PHUIIconView()) - ->setIconFont($part['icon']); + ->setIconFont($part['icon'].' bluegrey'); $name = phutil_tag( 'span', array( diff --git a/webroot/rsrc/css/aphront/table-view.css b/webroot/rsrc/css/aphront/table-view.css index 8b31b33b8d..e0e06317c8 100644 --- a/webroot/rsrc/css/aphront/table-view.css +++ b/webroot/rsrc/css/aphront/table-view.css @@ -102,7 +102,7 @@ th.aphront-table-view-sortable-selected { */ .aphront-table-view th { - padding: 10px 10px; + padding: 8px 10px; font-size: {$normalfontsize}; } diff --git a/webroot/rsrc/css/core/remarkup.css b/webroot/rsrc/css/core/remarkup.css index 98dad91a26..5c6cfd8c89 100644 --- a/webroot/rsrc/css/core/remarkup.css +++ b/webroot/rsrc/css/core/remarkup.css @@ -39,7 +39,7 @@ } .phabricator-remarkup .remarkup-code-block pre { - background: #FFFEF5; + background: #FEF9ED; border: 1px solid {$sh-lightyellowborder}; display: block; color: #000;