diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -215,6 +215,7 @@ 'ArcanistLandWorkflow' => 'workflow/ArcanistLandWorkflow.php', 'ArcanistLanguageConstructParenthesesXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistLanguageConstructParenthesesXHPASTLinterRule.php', 'ArcanistLanguageConstructParenthesesXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistLanguageConstructParenthesesXHPASTLinterRuleTestCase.php', + 'ArcanistLeaseWorkflow' => 'workflow/ArcanistLeaseWorkflow.php', 'ArcanistLesscLinter' => 'lint/linter/ArcanistLesscLinter.php', 'ArcanistLesscLinterTestCase' => 'lint/linter/__tests__/ArcanistLesscLinterTestCase.php', 'ArcanistLiberateWorkflow' => 'workflow/ArcanistLiberateWorkflow.php', @@ -306,6 +307,7 @@ 'ArcanistPyLintLinterTestCase' => 'lint/linter/__tests__/ArcanistPyLintLinterTestCase.php', 'ArcanistRaggedClassTreeEdgeXHPASTLinterRule' => 'lint/linter/xhpast/rules/ArcanistRaggedClassTreeEdgeXHPASTLinterRule.php', 'ArcanistRaggedClassTreeEdgeXHPASTLinterRuleTestCase' => 'lint/linter/xhpast/rules/__tests__/ArcanistRaggedClassTreeEdgeXHPASTLinterRuleTestCase.php', + 'ArcanistReleaseWorkflow' => 'workflow/ArcanistReleaseWorkflow.php', 'ArcanistRepositoryAPI' => 'repository/api/ArcanistRepositoryAPI.php', 'ArcanistRepositoryAPIMiscTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIMiscTestCase.php', 'ArcanistRepositoryAPIStateTestCase' => 'repository/api/__tests__/ArcanistRepositoryAPIStateTestCase.php', @@ -629,6 +631,7 @@ 'ArcanistLandWorkflow' => 'ArcanistWorkflow', 'ArcanistLanguageConstructParenthesesXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistLanguageConstructParenthesesXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistLeaseWorkflow' => 'ArcanistWorkflow', 'ArcanistLesscLinter' => 'ArcanistExternalLinter', 'ArcanistLesscLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistLiberateWorkflow' => 'ArcanistWorkflow', @@ -720,6 +723,7 @@ 'ArcanistPyLintLinterTestCase' => 'ArcanistExternalLinterTestCase', 'ArcanistRaggedClassTreeEdgeXHPASTLinterRule' => 'ArcanistXHPASTLinterRule', 'ArcanistRaggedClassTreeEdgeXHPASTLinterRuleTestCase' => 'ArcanistXHPASTLinterRuleTestCase', + 'ArcanistReleaseWorkflow' => 'ArcanistWorkflow', 'ArcanistRepositoryAPI' => 'Phobject', 'ArcanistRepositoryAPIMiscTestCase' => 'PhutilTestCase', 'ArcanistRepositoryAPIStateTestCase' => 'PhutilTestCase', diff --git a/src/workflow/ArcanistLeaseWorkflow.php b/src/workflow/ArcanistLeaseWorkflow.php new file mode 100644 --- /dev/null +++ b/src/workflow/ArcanistLeaseWorkflow.php @@ -0,0 +1,214 @@ + array( + 'help' => pht('ID of the blueprint to use when acquiring a lease.'), + 'param' => 'id', + ), + 'wait' => array( + 'help' => pht('Block until the lease is active.'), + ), + '*' => 'attributes', + ); + } + + public function requiresAuthentication() { + return true; + } + + public function run() { + if (!$this->getPassedArguments()) { + $this->listAuthorizations(); + return 0; + } + + $attributes = $this->getArgument('attributes'); + if (count($attributes)) { + $file = head($attributes); + if ($file === '-') { + $json = file_get_contents('php://stdin'); + } else { + $json = Filesystem::readFile($file); + } + $attributes = phutil_json_decode($json); + } + + $blueprint_id = (int)$this->getArgument('blueprint'); + if (!$blueprint_id) { + throw new ArcanistUsageException('You must provide a blueprint ID.'); + } + $blueprint = $this->getConduit()->callMethodSynchronous( + 'drydock.blueprint.search', + array( + 'constraints' => array( + 'ids' => array($blueprint_id), + ), + )); + $blueprint = $blueprint['data'][0]; + try { + $lease = $this->getConduit()->callMethodSynchronous( + 'drydock.createlease', + array( + 'blueprintPHID' => $blueprint['phid'], + 'leaseAttributes' => $attributes, + )); + } catch (ConduitClientException $e) { + if ($e->getErrorCode() === 'ERR_NOT_AUTHORIZED') { + if (phutil_console_confirm(pht( + 'You are not authorized to use this blueprint, request '. + 'authorization?'))) { + $this->requestAuthorization($blueprint['phid']); + return 1; + } + } + throw $e; + } + + echo phutil_console_format( + "%s\n **%s** __%s__\n\n", + pht('Lease queued for activation.'), + pht('Lease URI:'), + rtrim($this->getConfigFromAnySource('phabricator.uri'), '/').'/'. + 'drydock/lease/'.$lease['id'].'/'); + + if ($this->getArgument('wait')) { + $this->waitUntilActive($lease['phid']); + } + + return 0; + } + + private function waitUntilActive($lease_phid) { + $lease = $this->getConduit()->callMethodSynchronous( + 'drydock.lease.search', + array( + 'constraints' => array( + 'phids' => array($lease_phid), + ), + )); + $lease_status = idxv($lease, array('data', 0, 'fields', 'status', 'value')); + switch ($lease_status) { + case 'pending': + case 'acquired': + sleep(1); + $this->waitUntilActive($lease_phid); + break; + case 'active': + return; + case 'released': + case 'broken': + case 'destroyed': + throw new Exception(pht('Lease failed to activate.')); + } + } + + private function requestAuthorization($blueprint_phid) { + $console = PhutilConsole::getConsole(); + $authorization = $this->getConduit()->callMethodSynchronous( + 'drydock.requestauthorization', + array( + 'blueprintPHID' => $blueprint_phid, + )); + echo phutil_console_format( + "%s\n **%s** __%s__\n\n", + pht('Authorization request issued.'), + pht('Request URI:'), + rtrim($this->getConfigFromAnySource('phabricator.uri'), '/').'/'. + 'drydock/authorization/'.$authorization['id'].'/'); + } + + private function listAuthorizations() { + $console = PhutilConsole::getConsole(); + $console->writeOut(" %s \n", pht('BLUEPRINT AUTHORIZATIONS')); + $conduit = $this->getConduit(); + $authorizations = $conduit->callMethodSynchronous( + 'drydock.authorization.search', + array( + 'constraints' => array( + 'objectPHIDs' => array($this->getUserPHID()), + ), + )); + $authorizations = idx($authorizations, 'data'); + + if (!$authorizations) { + $console->writeOut("\t%s\n", pht('No authorizations found.')); + } + + $authorization_fields = ipull($authorizations, 'fields'); + $blueprint_phids = ipull($authorization_fields, 'blueprintPHID'); + + $blueprints = $conduit->callMethodSynchronous( + 'drydock.blueprint.search', + array( + 'constraints' => array( + 'phids' => $blueprint_phids, + ), + )); + $blueprints = idx($blueprints, 'data'); + $blueprints = ipull($blueprints, null, 'phid'); + + $table = (new PhutilConsoleTable()) + ->addColumn('id', array('title' => 'ID')) + ->addColumn('name', array('title' => 'Name')) + ->addColumn('type', array('title' => 'Type')) + ->addColumn('authorization', array('title' => 'Authorization')) + ->setBorders(true); + + foreach ($authorizations as $authorization) { + $blueprint_phid = idxv($authorization, array('fields', 'blueprintPHID')); + $blueprint = idx($blueprints, $blueprint_phid); + $table->addRow(array( + 'id' => idx($blueprint, 'id'), + 'name' => idxv($blueprint, array('fields', 'name')), + 'type' => idxv($blueprint, array('fields', 'type')), + 'authorization' => idxv($authorization, array( + 'fields', + 'blueprintAuthorizationState', + 'name', + )), + )); + } + + $table->draw(); + } + +} diff --git a/src/workflow/ArcanistReleaseWorkflow.php b/src/workflow/ArcanistReleaseWorkflow.php new file mode 100644 --- /dev/null +++ b/src/workflow/ArcanistReleaseWorkflow.php @@ -0,0 +1,108 @@ + 'lease', + ); + } + + public function requiresAuthentication() { + return true; + } + + public function run() { + if (!$this->getPassedArguments()) { + $this->listLeases(); + return 0; + } + + $lease_id = (int)head($this->getArgument('lease')); + if (!$lease_id) { + throw new ArcanistUsageException('You must provide a lease ID.'); + } + + $lease = $this->getConduit()->callMethodSynchronous( + 'drydock.lease.search', + array( + 'constraints' => array( + 'ids' => array($lease_id), + ), + )); + $lease = $lease['data'][0]; + + $this->getConduit()->callMethodSynchronous('drydock.destroylease', array( + 'leasePHID' => $lease['phid'], + )); + + echo phutil_console_format( + "%s\n **%s** __%s__\n\n", + pht('Lease scheduled for release.'), + pht('Lease URI:'), + rtrim($this->getConfigFromAnySource('phabricator.uri'), '/').'/'. + 'drydock/lease/'.$lease['id'].'/'); + + return 0; + } + + protected function listLeases() { + $console = PhutilConsole::getConsole(); + $console->writeOut(" %s \n", pht('LIVE LEASES')); + $conduit = $this->getConduit(); + $leases = $conduit->callMethodSynchronous('drydock.lease.search', array( + 'constraints' => array( + 'statuses' => array( + 'pending', + 'acquired', + 'active', + ), + 'ownerPHIDs' => array( + $this->getUserPHID(), + ), + ), + )); + $leases = idx($leases, 'data', array()); + + if (!$leases) { + $console->writeOut("\t%s\n", pht('No leases found.')); + return; + } + + $table = (new PhutilConsoleTable()) + ->addColumn('id', array('title' => 'ID')) + ->addColumn('status', array('title' => 'Status')) + ->addColumn('type', array('title' => 'Type')) + ->setBorders(true); + + foreach ($leases as $lease) { + $table->addRow(array( + 'id' => idx($lease, 'id'), + 'status' => idxv($lease, array('fields', 'status', 'name')), + 'type' => idxv($lease, array('fields', 'resourceType')), + )); + } + + $table->draw(); + } + +}