diff --git a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php index d729d868b0..ddab677faf 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildActionController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildActionController.php @@ -1,160 +1,162 @@ getViewer(); $id = $request->getURIData('id'); $action = $request->getURIData('action'); $via = $request->getURIData('via'); $build = id(new HarbormasterBuildQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$build) { return new Aphront404Response(); } switch ($action) { case HarbormasterBuildCommand::COMMAND_RESTART: $can_issue = $build->canRestartBuild(); break; 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(); } + $build->assertCanIssueCommand($viewer, $action); + switch ($via) { case 'buildable': $return_uri = '/'.$build->getBuildable()->getMonogram(); break; default: $return_uri = $this->getApplicationURI('/build/'.$build->getID().'/'); break; } if ($request->isDialogFormPost() && $can_issue) { $editor = id(new HarbormasterBuildTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $xaction = id(new HarbormasterBuildTransaction()) ->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND) ->setNewValue($action); $editor->applyTransactions($build, array($xaction)); return id(new AphrontRedirectResponse())->setURI($return_uri); } switch ($action) { case HarbormasterBuildCommand::COMMAND_RESTART: if ($can_issue) { $title = pht('Really restart build?'); $body = pht( 'Progress on this build will be discarded and the build will '. 'restart. Side effects of the build will occur again. Really '. 'restart build?'); $submit = pht('Restart Build'); } else { $title = pht('Unable to Restart Build'); if ($build->isRestarting()) { $body = pht( 'This build is already restarting. You can not reissue a '. 'restart command to a restarting build.'); } else { $body = pht('You can not restart this build.'); } } break; 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( 'If you pause this build, work will halt once the current steps '. 'complete. You can resume the build later.'); $submit = pht('Pause Build'); } else { $title = pht('Unable to Pause Build'); if ($build->isComplete()) { $body = pht( 'This build is already complete. You can not pause a completed '. 'build.'); } 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->isPausing()) { $body = pht( 'This build is already pausing. You can not reissue a pause '. 'command to a pausing build.'); } else { $body = pht( 'This build can not be paused.'); } } break; case HarbormasterBuildCommand::COMMAND_RESUME: if ($can_issue) { $title = pht('Really resume build?'); $body = pht( 'Work will continue on the build. Really resume?'); $submit = pht('Resume Build'); } else { $title = pht('Unable to Resume Build'); if ($build->isResuming()) { $body = pht( 'This build is already resuming. You can not reissue a resume '. 'command to a resuming build.'); } else if (!$build->isPaused()) { $body = pht( 'This build is not paused. You can only resume a paused '. 'build.'); } } break; } $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle($title) ->appendChild($body) ->addCancelButton($return_uri); if ($can_issue) { $dialog->addSubmitButton($submit); } return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php index a9cbb43ccf..25f3506c0b 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildViewController.php @@ -1,619 +1,638 @@ getRequest(); $viewer = $request->getUser(); $id = $request->getURIData('id'); $generation = $request->getInt('g'); $build = id(new HarbormasterBuildQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); if (!$build) { return new Aphront404Response(); } require_celerity_resource('harbormaster-css'); $title = pht('Build %d', $id); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($viewer) ->setPolicyObject($build); if ($build->isRestarting()) { $header->setStatus('fa-exclamation-triangle', 'red', pht('Restarting')); } 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()) ->setHeader($header); $actions = $this->buildActionList($build); $this->buildPropertyLists($box, $build, $actions); $crumbs = $this->buildApplicationCrumbs(); $this->addBuildableCrumb($crumbs, $build->getBuildable()); $crumbs->addTextCrumb($title); if ($generation === null || $generation > $build->getBuildGeneration() || $generation < 0) { $generation = $build->getBuildGeneration(); } $build_targets = id(new HarbormasterBuildTargetQuery()) ->setViewer($viewer) ->needBuildSteps(true) ->withBuildPHIDs(array($build->getPHID())) ->withBuildGenerations(array($generation)) ->execute(); if ($build_targets) { $messages = id(new HarbormasterBuildMessageQuery()) ->setViewer($viewer) ->withBuildTargetPHIDs(mpull($build_targets, 'getPHID')) ->execute(); $messages = mgroup($messages, 'getBuildTargetPHID'); } else { $messages = array(); } if ($build_targets) { $artifacts = id(new HarbormasterBuildArtifactQuery()) ->setViewer($viewer) ->withBuildTargetPHIDs(mpull($build_targets, 'getPHID')) ->execute(); $artifacts = msort($artifacts, 'getArtifactKey'); $artifacts = mgroup($artifacts, 'getBuildTargetPHID'); } else { $artifacts = array(); } $targets = array(); foreach ($build_targets as $build_target) { $header = id(new PHUIHeaderView()) ->setHeader($build_target->getName()) ->setUser($viewer); $target_box = id(new PHUIObjectBoxView()) ->setHeader($header); $properties = new PHUIPropertyListView(); $target_artifacts = idx($artifacts, $build_target->getPHID(), array()); $links = array(); $type_uri = HarbormasterURIArtifact::ARTIFACTCONST; foreach ($target_artifacts as $artifact) { if ($artifact->getArtifactType() == $type_uri) { $impl = $artifact->getArtifactImplementation(); if ($impl->isExternalLink()) { $links[] = $impl->renderLink(); } } } if ($links) { $links = phutil_implode_html(phutil_tag('br'), $links); $properties->addProperty( pht('External Link'), $links); } $status_view = new PHUIStatusListView(); $item = new PHUIStatusItemView(); $status = $build_target->getTargetStatus(); $status_name = HarbormasterBuildTarget::getBuildTargetStatusName($status); $icon = HarbormasterBuildTarget::getBuildTargetStatusIcon($status); $color = HarbormasterBuildTarget::getBuildTargetStatusColor($status); $item->setTarget($status_name); $item->setIcon($icon, $color); $status_view->addItem($item); $when = array(); $started = $build_target->getDateStarted(); $now = PhabricatorTime::getNow(); if ($started) { $ended = $build_target->getDateCompleted(); if ($ended) { $when[] = pht( 'Completed at %s', phabricator_datetime($started, $viewer)); $duration = ($ended - $started); if ($duration) { $when[] = pht( 'Built for %s', phutil_format_relative_time_detailed($duration)); } else { $when[] = pht('Built instantly'); } } else { $when[] = pht( 'Started at %s', phabricator_datetime($started, $viewer)); $duration = ($now - $started); if ($duration) { $when[] = pht( 'Running for %s', phutil_format_relative_time_detailed($duration)); } } } else { $created = $build_target->getDateCreated(); $when[] = pht( 'Queued at %s', phabricator_datetime($started, $viewer)); $duration = ($now - $created); if ($duration) { $when[] = pht( 'Waiting for %s', phutil_format_relative_time_detailed($duration)); } } $properties->addProperty( pht('When'), phutil_implode_html(" \xC2\xB7 ", $when)); $properties->addProperty(pht('Status'), $status_view); $target_box->addPropertyList($properties, pht('Overview')); $step = $build_target->getBuildStep(); if ($step) { $description = $step->getDescription(); if ($description) { $description = new PHUIRemarkupView($viewer, $description); $properties->addSectionHeader( pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $properties->addTextContent($description); } } else { $target_box->setFormErrors( array( pht( 'This build step has since been deleted on the build plan. '. 'Some information may be omitted.'), )); } $details = $build_target->getDetails(); $properties = new PHUIPropertyListView(); foreach ($details as $key => $value) { $properties->addProperty($key, $value); } $target_box->addPropertyList($properties, pht('Configuration')); $variables = $build_target->getVariables(); $properties = new PHUIPropertyListView(); $properties->addRawContent($this->buildProperties($variables)); $target_box->addPropertyList($properties, pht('Variables')); $artifacts_tab = $this->buildArtifacts($build_target, $target_artifacts); $properties = new PHUIPropertyListView(); $properties->addRawContent($artifacts_tab); $target_box->addPropertyList($properties, pht('Artifacts')); $build_messages = idx($messages, $build_target->getPHID(), array()); $properties = new PHUIPropertyListView(); $properties->addRawContent($this->buildMessages($build_messages)); $target_box->addPropertyList($properties, pht('Messages')); $properties = new PHUIPropertyListView(); $properties->addProperty( pht('Build Target ID'), $build_target->getID()); $properties->addProperty( pht('Build Target PHID'), $build_target->getPHID()); $target_box->addPropertyList($properties, pht('Metadata')); $targets[] = $target_box; $targets[] = $this->buildLog($build, $build_target); } $timeline = $this->buildTransactionTimeline( $build, new HarbormasterBuildTransactionQuery()); $timeline->setShouldTerminate(true); return $this->buildApplicationPage( array( $crumbs, $box, $targets, $timeline, ), array( 'title' => $title, )); } private function buildArtifacts( HarbormasterBuildTarget $build_target, array $artifacts) { $viewer = $this->getViewer(); $rows = array(); foreach ($artifacts as $artifact) { $impl = $artifact->getArtifactImplementation(); if ($impl) { $summary = $impl->renderArtifactSummary($viewer); $type_name = $impl->getArtifactTypeName(); } else { $summary = pht(''); $type_name = $artifact->getType(); } $rows[] = array( $artifact->getArtifactKey(), $type_name, $summary, ); } $table = id(new AphrontTableView($rows)) ->setNoDataString(pht('This target has no associated artifacts.')) ->setHeaders( array( pht('Key'), pht('Type'), pht('Summary'), )) ->setColumnClasses( array( 'pri', '', 'wide', )); return $table; } private function buildLog( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { $request = $this->getRequest(); $viewer = $request->getUser(); $limit = $request->getInt('l', 25); $logs = id(new HarbormasterBuildLogQuery()) ->setViewer($viewer) ->withBuildTargetPHIDs(array($build_target->getPHID())) ->execute(); $empty_logs = array(); $log_boxes = array(); foreach ($logs as $log) { $start = 1; $lines = preg_split("/\r\n|\r|\n/", $log->getLogText()); if ($limit !== 0) { $start = count($lines) - $limit; if ($start >= 1) { $lines = array_slice($lines, -$limit, $limit); } else { $start = 1; } } $id = null; $is_empty = false; if (count($lines) === 1 && trim($lines[0]) === '') { // Prevent Harbormaster from showing empty build logs. $id = celerity_generate_unique_node_id(); $empty_logs[] = $id; $is_empty = true; } $log_view = new ShellLogView(); $log_view->setLines($lines); $log_view->setStart($start); $header = id(new PHUIHeaderView()) ->setHeader(pht( 'Build Log %d (%s - %s)', $log->getID(), $log->getLogSource(), $log->getLogType())) ->setSubheader($this->createLogHeader($build, $log)) ->setUser($viewer); $log_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->setForm($log_view); if ($is_empty) { $log_box = phutil_tag( 'div', array( 'style' => 'display: none', 'id' => $id, ), $log_box); } $log_boxes[] = $log_box; } if ($empty_logs) { $hide_id = celerity_generate_unique_node_id(); Javelin::initBehavior('phabricator-reveal-content'); $expand = phutil_tag( 'div', array( 'id' => $hide_id, 'class' => 'harbormaster-empty-logs-are-hidden mlr mlt mll', ), array( pht( '%s empty logs are hidden.', phutil_count($empty_logs)), ' ', javelin_tag( 'a', array( 'href' => '#', 'sigil' => 'reveal-content', 'meta' => array( 'showIDs' => $empty_logs, 'hideIDs' => array($hide_id), ), ), pht('Show all logs.')), )); array_unshift($log_boxes, $expand); } return $log_boxes; } private function createLogHeader($build, $log) { $request = $this->getRequest(); $limit = $request->getInt('l', 25); $lines_25 = $this->getApplicationURI('/build/'.$build->getID().'/?l=25'); $lines_50 = $this->getApplicationURI('/build/'.$build->getID().'/?l=50'); $lines_100 = $this->getApplicationURI('/build/'.$build->getID().'/?l=100'); $lines_0 = $this->getApplicationURI('/build/'.$build->getID().'/?l=0'); $link_25 = phutil_tag('a', array('href' => $lines_25), pht('25')); $link_50 = phutil_tag('a', array('href' => $lines_50), pht('50')); $link_100 = phutil_tag('a', array('href' => $lines_100), pht('100')); $link_0 = phutil_tag('a', array('href' => $lines_0), pht('Unlimited')); if ($limit === 25) { $link_25 = phutil_tag('strong', array(), $link_25); } else if ($limit === 50) { $link_50 = phutil_tag('strong', array(), $link_50); } else if ($limit === 100) { $link_100 = phutil_tag('strong', array(), $link_100); } else if ($limit === 0) { $link_0 = phutil_tag('strong', array(), $link_0); } return phutil_tag( 'span', array(), array( $link_25, ' - ', $link_50, ' - ', $link_100, ' - ', $link_0, ' Lines', )); } private function buildActionList(HarbormasterBuild $build) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $build->getID(); $list = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($build); - $can_restart = $build->canRestartBuild(); - $can_pause = $build->canPauseBuild(); - $can_resume = $build->canResumeBuild(); - $can_abort = $build->canAbortBuild(); + $can_restart = + $build->canRestartBuild() && + $build->canIssueCommand( + $viewer, + HarbormasterBuildCommand::COMMAND_RESTART); + + $can_pause = + $build->canPauseBuild() && + $build->canIssueCommand( + $viewer, + HarbormasterBuildCommand::COMMAND_PAUSE); + + $can_resume = + $build->canResumeBuild() && + $build->canIssueCommand( + $viewer, + HarbormasterBuildCommand::COMMAND_RESUME); + + $can_abort = + $build->canAbortBuild() && + $build->canIssueCommand( + $viewer, + HarbormasterBuildCommand::COMMAND_ABORT); $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Restart Build')) ->setIcon('fa-repeat') ->setHref($this->getApplicationURI('/build/restart/'.$id.'/')) ->setDisabled(!$can_restart) ->setWorkflow(true)); if ($build->canResumeBuild()) { $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Resume Build')) ->setIcon('fa-play') ->setHref($this->getApplicationURI('/build/resume/'.$id.'/')) ->setDisabled(!$can_resume) ->setWorkflow(true)); } else { $list->addAction( id(new PhabricatorActionView()) ->setName(pht('Pause Build')) ->setIcon('fa-pause') ->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; } private function buildPropertyLists( PHUIObjectBoxView $box, HarbormasterBuild $build, PhabricatorActionListView $actions) { $request = $this->getRequest(); $viewer = $request->getUser(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($build) ->setActionList($actions); $box->addPropertyList($properties); $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(array( $build->getBuildablePHID(), $build->getBuildPlanPHID(), )) ->execute(); $properties->addProperty( pht('Buildable'), $handles[$build->getBuildablePHID()]->renderLink()); $properties->addProperty( pht('Build Plan'), $handles[$build->getBuildPlanPHID()]->renderLink()); $properties->addProperty( pht('Restarts'), $build->getBuildGeneration()); $properties->addProperty( pht('Status'), $this->getStatus($build)); } private function getStatus(HarbormasterBuild $build) { $status_view = new PHUIStatusListView(); $item = new PHUIStatusItemView(); if ($build->isPausing()) { $status_name = pht('Pausing'); $icon = PHUIStatusItemView::ICON_RIGHT; $color = 'dark'; } else { $status = $build->getBuildStatus(); $status_name = HarbormasterBuild::getBuildStatusName($status); $icon = HarbormasterBuild::getBuildStatusIcon($status); $color = HarbormasterBuild::getBuildStatusColor($status); } $item->setTarget($status_name); $item->setIcon($icon, $color); $status_view->addItem($item); return $status_view; } private function buildMessages(array $messages) { $viewer = $this->getRequest()->getUser(); if ($messages) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($viewer) ->withPHIDs(mpull($messages, 'getAuthorPHID')) ->execute(); } else { $handles = array(); } $rows = array(); foreach ($messages as $message) { $rows[] = array( $message->getID(), $handles[$message->getAuthorPHID()]->renderLink(), $message->getType(), $message->getIsConsumed() ? pht('Consumed') : null, phabricator_datetime($message->getDateCreated(), $viewer), ); } $table = new AphrontTableView($rows); $table->setNoDataString(pht('No messages for this build target.')); $table->setHeaders( array( pht('ID'), pht('From'), pht('Type'), pht('Consumed'), pht('Received'), )); $table->setColumnClasses( array( '', '', 'wide', '', 'date', )); return $table; } private function buildProperties(array $properties) { ksort($properties); $rows = array(); foreach ($properties as $key => $value) { $rows[] = array( $key, $value, ); } $table = id(new AphrontTableView($rows)) ->setHeaders( array( pht('Key'), pht('Value'), )) ->setColumnClasses( array( 'pri right', 'wide', )); return $table; } } diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php index 9fa7d6f006..b3f60cd1dc 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableActionController.php @@ -1,147 +1,207 @@ getViewer(); $id = $request->getURIData('id'); $action = $request->getURIData('action'); $buildable = id(new HarbormasterBuildableQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needBuilds(true) ->requireCapabilities( array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, )) ->executeOne(); if (!$buildable) { return new Aphront404Response(); } $issuable = array(); foreach ($buildable->getBuilds() as $build) { switch ($action) { case HarbormasterBuildCommand::COMMAND_RESTART: if ($build->canRestartBuild()) { $issuable[] = $build; } break; case HarbormasterBuildCommand::COMMAND_PAUSE: if ($build->canPauseBuild()) { $issuable[] = $build; } break; case HarbormasterBuildCommand::COMMAND_RESUME: if ($build->canResumeBuild()) { $issuable[] = $build; } break; case HarbormasterBuildCommand::COMMAND_ABORT: if ($build->canAbortBuild()) { $issuable[] = $build; } break; default: return new Aphront400Response(); } } + $restricted = false; + foreach ($issuable as $key => $build) { + if (!$build->canIssueCommand($viewer, $action)) { + $restricted = true; + unset($issuable[$key]); + } + } + $return_uri = '/'.$buildable->getMonogram(); if ($request->isDialogFormPost() && $issuable) { $editor = id(new HarbormasterBuildableTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $xaction = id(new HarbormasterBuildableTransaction()) ->setTransactionType(HarbormasterBuildableTransaction::TYPE_COMMAND) ->setNewValue($action); $editor->applyTransactions($buildable, array($xaction)); $build_editor = id(new HarbormasterBuildTransactionEditor()) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); foreach ($issuable as $build) { $xaction = id(new HarbormasterBuildTransaction()) ->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND) ->setNewValue($action); $build_editor->applyTransactions($build, array($xaction)); } return id(new AphrontRedirectResponse())->setURI($return_uri); } switch ($action) { case HarbormasterBuildCommand::COMMAND_RESTART: if ($issuable) { - $title = pht('Really restart all builds?'); - $body = pht( - 'Progress on all builds will be discarded, and all builds will '. - 'restart. Side effects of the builds will occur again. Really '. - 'restart all builds?'); - $submit = pht('Restart All Builds'); + $title = pht('Really restart builds?'); + + if ($restricted) { + $body = pht( + 'You only have permission to restart some builds. Progress '. + 'on builds you have permission to restart will be discarded '. + 'and they will restart. Side effects of these builds will '. + 'occur again. Really restart all builds?'); + } else { + $body = pht( + 'Progress on all builds will be discarded, and all builds will '. + 'restart. Side effects of the builds will occur again. Really '. + 'restart all builds?'); + } + + $submit = pht('Restart Builds'); } else { $title = pht('Unable to Restart Builds'); - $body = pht('No builds can be restarted.'); + + if ($restricted) { + $body = pht('You do not have permission to restart any builds.'); + } else { + $body = pht('No builds can be restarted.'); + } } break; case HarbormasterBuildCommand::COMMAND_PAUSE: if ($issuable) { - $title = pht('Really pause all builds?'); - $body = pht( - 'If you pause all builds, work will halt once the current steps '. - 'complete. You can resume the builds later.'); - $submit = pht('Pause All Builds'); + $title = pht('Really pause builds?'); + + if ($restricted) { + $body = pht( + 'You only have permission to pause some builds. Once the '. + 'current steps complete, work will halt on builds you have '. + 'permission to pause. You can resume the builds later.'); + } else { + $body = pht( + 'If you pause all builds, work will halt once the current steps '. + 'complete. You can resume the builds later.'); + } + $submit = pht('Pause Builds'); } else { $title = pht('Unable to Pause Builds'); - $body = pht('No builds can be paused.'); + + if ($restricted) { + $body = pht('You do not have permission to pause any builds.'); + } else { + $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'); + $title = pht('Really abort builds?'); + if ($restricted) { + $body = pht( + 'You only have permission to abort some builds. Work will '. + 'halt immediately on builds you have permission to abort. '. + 'Progress will be discarded, and builds must be completely '. + 'restarted if you want them to complete.'); + } else { + $body = pht( + 'If you abort all builds, work will halt immediately. Work '. + 'will be discarded, and builds must be completely restarted.'); + } + $submit = pht('Abort Builds'); } else { $title = pht('Unable to Abort Builds'); - $body = pht('No builds can be aborted.'); + + if ($restricted) { + $body = pht('You do not have permission to abort any builds.'); + } else { + $body = pht('No builds can be aborted.'); + } } break; case HarbormasterBuildCommand::COMMAND_RESUME: if ($issuable) { - $title = pht('Really resume all builds?'); - $body = pht('Work will continue on all builds. Really resume?'); - $submit = pht('Resume All Builds'); + $title = pht('Really resume builds?'); + if ($restricted) { + $body = pht( + 'You only have permission to resume some builds. Work will '. + 'continue on builds you have permission to resume.'); + } else { + $body = pht('Work will continue on all builds. Really resume?'); + } + + $submit = pht('Resume Builds'); } else { $title = pht('Unable to Resume Builds'); - $body = pht('No builds can be resumed.'); + if ($restricted) { + $body = pht('You do not have permission to resume any builds.'); + } else { + $body = pht('No builds can be resumed.'); + } } break; } $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setTitle($title) ->appendChild($body) ->addCancelButton($return_uri); if ($issuable) { $dialog->addSubmitButton($submit); } return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php index f9a0807ab1..91fb45176f 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php @@ -1,353 +1,366 @@ getViewer(); $buildable = id(new HarbormasterBuildableQuery()) ->setViewer($viewer) ->withIDs(array($request->getURIData('id'))) ->needBuildableHandles(true) ->needContainerHandles(true) ->executeOne(); if (!$buildable) { return new Aphront404Response(); } $id = $buildable->getID(); // Pull builds and build targets. $builds = id(new HarbormasterBuildQuery()) ->setViewer($viewer) ->withBuildablePHIDs(array($buildable->getPHID())) ->needBuildTargets(true) ->execute(); list($lint, $unit) = $this->renderLintAndUnit($buildable, $builds); $buildable->attachBuilds($builds); $object = $buildable->getBuildableObject(); $build_list = $this->buildBuildList($buildable); $title = pht('Buildable %d', $id); $header = id(new PHUIHeaderView()) ->setHeader($title) ->setUser($viewer) ->setPolicyObject($buildable); $box = id(new PHUIObjectBoxView()) ->setHeader($header); $timeline = $this->buildTransactionTimeline( $buildable, new HarbormasterBuildableTransactionQuery()); $timeline->setShouldTerminate(true); $actions = $this->buildActionList($buildable); $this->buildPropertyLists($box, $buildable, $actions); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($buildable->getMonogram()); return $this->buildApplicationPage( array( $crumbs, $box, $lint, $unit, $build_list, $timeline, ), array( 'title' => $title, )); } private function buildActionList(HarbormasterBuildable $buildable) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $buildable->getID(); $list = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($buildable); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $buildable, PhabricatorPolicyCapability::CAN_EDIT); $can_restart = false; $can_resume = false; $can_pause = false; $can_abort = false; + $command_restart = HarbormasterBuildCommand::COMMAND_RESTART; + $command_resume = HarbormasterBuildCommand::COMMAND_RESUME; + $command_pause = HarbormasterBuildCommand::COMMAND_PAUSE; + $command_abort = HarbormasterBuildCommand::COMMAND_ABORT; + foreach ($buildable->getBuilds() as $build) { if ($build->canRestartBuild()) { - $can_restart = true; + if ($build->canIssueCommand($viewer, $command_restart)) { + $can_restart = true; + } } if ($build->canResumeBuild()) { - $can_resume = true; + if ($build->canIssueCommand($viewer, $command_resume)) { + $can_resume = true; + } } if ($build->canPauseBuild()) { - $can_pause = true; + if ($build->canIssueCommand($viewer, $command_pause)) { + $can_pause = true; + } } if ($build->canAbortBuild()) { - $can_abort = true; + if ($build->canIssueCommand($viewer, $command_abort)) { + $can_abort = true; + } } } $restart_uri = "buildable/{$id}/restart/"; $pause_uri = "buildable/{$id}/pause/"; $resume_uri = "buildable/{$id}/resume/"; $abort_uri = "buildable/{$id}/abort/"; $list->addAction( id(new PhabricatorActionView()) ->setIcon('fa-repeat') ->setName(pht('Restart All Builds')) ->setHref($this->getApplicationURI($restart_uri)) ->setWorkflow(true) ->setDisabled(!$can_restart || !$can_edit)); $list->addAction( id(new PhabricatorActionView()) ->setIcon('fa-pause') ->setName(pht('Pause All Builds')) ->setHref($this->getApplicationURI($pause_uri)) ->setWorkflow(true) ->setDisabled(!$can_pause || !$can_edit)); $list->addAction( id(new PhabricatorActionView()) ->setIcon('fa-play') ->setName(pht('Resume All Builds')) ->setHref($this->getApplicationURI($resume_uri)) ->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; } private function buildPropertyLists( PHUIObjectBoxView $box, HarbormasterBuildable $buildable, PhabricatorActionListView $actions) { $request = $this->getRequest(); $viewer = $request->getUser(); $properties = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($buildable) ->setActionList($actions); $box->addPropertyList($properties); if ($buildable->getContainerHandle() !== null) { $properties->addProperty( pht('Container'), $buildable->getContainerHandle()->renderLink()); } $properties->addProperty( pht('Buildable'), $buildable->getBuildableHandle()->renderLink()); $properties->addProperty( pht('Origin'), $buildable->getIsManualBuildable() ? pht('Manual Buildable') : pht('Automatic Buildable')); } private function buildBuildList(HarbormasterBuildable $buildable) { $viewer = $this->getRequest()->getUser(); $build_list = id(new PHUIObjectItemListView()) ->setUser($viewer); foreach ($buildable->getBuilds() as $build) { $view_uri = $this->getApplicationURI('/build/'.$build->getID().'/'); $item = id(new PHUIObjectItemView()) ->setObjectName(pht('Build %d', $build->getID())) ->setHeader($build->getName()) ->setHref($view_uri); $status = $build->getBuildStatus(); $item->setStatusIcon( 'fa-dot-circle-o '.HarbormasterBuild::getBuildStatusColor($status), HarbormasterBuild::getBuildStatusName($status)); $item->addAttribute(HarbormasterBuild::getBuildStatusName($status)); if ($build->isRestarting()) { $item->addIcon('fa-repeat', pht('Restarting')); } else if ($build->isPausing()) { $item->addIcon('fa-pause', pht('Pausing')); } else if ($build->isResuming()) { $item->addIcon('fa-play', pht('Resuming')); } $build_id = $build->getID(); $restart_uri = "build/restart/{$build_id}/buildable/"; $resume_uri = "build/resume/{$build_id}/buildable/"; $pause_uri = "build/pause/{$build_id}/buildable/"; $abort_uri = "build/abort/{$build_id}/buildable/"; $item->addAction( id(new PHUIListItemView()) ->setIcon('fa-repeat') ->setName(pht('Restart')) ->setHref($this->getApplicationURI($restart_uri)) ->setWorkflow(true) ->setDisabled(!$build->canRestartBuild())); if ($build->canResumeBuild()) { $item->addAction( id(new PHUIListItemView()) ->setIcon('fa-play') ->setName(pht('Resume')) ->setHref($this->getApplicationURI($resume_uri)) ->setWorkflow(true)); } else { $item->addAction( id(new PHUIListItemView()) ->setIcon('fa-pause') ->setName(pht('Pause')) ->setHref($this->getApplicationURI($pause_uri)) ->setWorkflow(true) ->setDisabled(!$build->canPauseBuild())); } $targets = $build->getBuildTargets(); if ($targets) { $target_list = id(new PHUIStatusListView()); foreach ($targets as $target) { $status = $target->getTargetStatus(); $icon = HarbormasterBuildTarget::getBuildTargetStatusIcon($status); $color = HarbormasterBuildTarget::getBuildTargetStatusColor($status); $status_name = HarbormasterBuildTarget::getBuildTargetStatusName($status); $name = $target->getName(); $target_list->addItem( id(new PHUIStatusItemView()) ->setIcon($icon, $color, $status_name) ->setTarget(pht('Target %d', $target->getID())) ->setNote($name)); } $target_box = id(new PHUIBoxView()) ->addPadding(PHUI::PADDING_SMALL) ->appendChild($target_list); $item->appendChild($target_box); } $build_list->addItem($item); } $build_list->setFlush(true); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Builds')) ->appendChild($build_list); return $box; } private function renderLintAndUnit( HarbormasterBuildable $buildable, array $builds) { $viewer = $this->getViewer(); $targets = array(); foreach ($builds as $build) { foreach ($build->getBuildTargets() as $target) { $targets[] = $target; } } if (!$targets) { return; } $target_phids = mpull($targets, 'getPHID'); $lint_data = id(new HarbormasterBuildLintMessage())->loadAllWhere( 'buildTargetPHID IN (%Ls)', $target_phids); $unit_data = id(new HarbormasterBuildUnitMessage())->loadAllWhere( 'buildTargetPHID IN (%Ls)', $target_phids); if ($lint_data) { $lint_table = id(new HarbormasterLintPropertyView()) ->setUser($viewer) ->setLimit(10) ->setLintMessages($lint_data); $lint_href = $this->getApplicationURI('lint/'.$buildable->getID().'/'); $lint_header = id(new PHUIHeaderView()) ->setHeader(pht('Lint Messages')) ->addActionLink( id(new PHUIButtonView()) ->setTag('a') ->setHref($lint_href) ->setIcon('fa-list-ul') ->setText('View All')); $lint = id(new PHUIObjectBoxView()) ->setHeader($lint_header) ->setTable($lint_table); } else { $lint = null; } if ($unit_data) { $unit_table = id(new HarbormasterUnitPropertyView()) ->setUser($viewer) ->setLimit(25) ->setUnitMessages($unit_data); $unit_href = $this->getApplicationURI('unit/'.$buildable->getID().'/'); $unit_header = id(new PHUIHeaderView()) ->setHeader(pht('Unit Tests')) ->addActionLink( id(new PHUIButtonView()) ->setTag('a') ->setHref($unit_href) ->setIcon('fa-list-ul') ->setText('View All')); $unit = id(new PHUIObjectBoxView()) ->setHeader($unit_header) ->setTable($unit_table); } else { $unit = null; } return array($lint, $unit); } } diff --git a/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php b/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php index 0727a473ec..2615f3c5be 100644 --- a/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php +++ b/src/applications/harbormaster/editor/HarbormasterBuildTransactionEditor.php @@ -1,120 +1,125 @@ getTransactionType()) { case HarbormasterBuildTransaction::TYPE_CREATE: case HarbormasterBuildTransaction::TYPE_COMMAND: return null; } return parent::getCustomTransactionOldValue($object, $xaction); } protected function getCustomTransactionNewValue( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case HarbormasterBuildTransaction::TYPE_CREATE: return true; case HarbormasterBuildTransaction::TYPE_COMMAND: return $xaction->getNewValue(); } return parent::getCustomTransactionNewValue($object, $xaction); } protected function applyCustomInternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case HarbormasterBuildTransaction::TYPE_CREATE: return; case HarbormasterBuildTransaction::TYPE_COMMAND: return $this->executeBuildCommand($object, $xaction); } return parent::applyCustomInternalTransaction($object, $xaction); } private function executeBuildCommand( HarbormasterBuild $build, HarbormasterBuildTransaction $xaction) { $command = $xaction->getNewValue(); switch ($command) { case HarbormasterBuildCommand::COMMAND_RESTART: $issuable = $build->canRestartBuild(); break; 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)); } if (!$issuable) { return; } + $actor = $this->getActor(); + if (!$build->canIssueCommand($actor, $command)) { + return; + } + id(new HarbormasterBuildCommand()) ->setAuthorPHID($xaction->getAuthorPHID()) ->setTargetPHID($build->getPHID()) ->setCommand($command) ->save(); PhabricatorWorker::scheduleTask( 'HarbormasterBuildWorker', array( 'buildID' => $build->getID(), ), array( 'objectPHID' => $build->getPHID(), )); } protected function applyCustomExternalTransaction( PhabricatorLiskDAO $object, PhabricatorApplicationTransaction $xaction) { switch ($xaction->getTransactionType()) { case HarbormasterBuildTransaction::TYPE_CREATE: case HarbormasterBuildTransaction::TYPE_COMMAND: return; } return parent::applyCustomExternalTransaction($object, $xaction); } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index c96d0c2710..1bac8d3ecd 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -1,501 +1,537 @@ setBuildStatus(self::STATUS_INACTIVE) ->setBuildGeneration(0); } public function delete() { $this->openTransaction(); $this->deleteUnprocessedCommands(); $result = parent::delete(); $this->saveTransaction(); return $result; } protected function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'buildParameters' => self::SERIALIZATION_JSON, ), self::CONFIG_COLUMN_SCHEMA => array( 'buildStatus' => 'text32', 'buildGeneration' => 'uint32', 'planAutoKey' => 'text32?', 'initiatorPHID' => 'phid?', ), self::CONFIG_KEY_SCHEMA => array( 'key_buildable' => array( 'columns' => array('buildablePHID'), ), 'key_plan' => array( 'columns' => array('buildPlanPHID'), ), 'key_status' => array( 'columns' => array('buildStatus'), ), 'key_planautokey' => array( 'columns' => array('buildablePHID', 'planAutoKey'), 'unique' => true, ), ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterBuildPHIDType::TYPECONST); } public function attachBuildable(HarbormasterBuildable $buildable) { $this->buildable = $buildable; return $this; } public function getBuildable() { return $this->assertAttached($this->buildable); } public function getName() { if ($this->getBuildPlan()) { return $this->getBuildPlan()->getName(); } return pht('Build'); } public function attachBuildPlan( HarbormasterBuildPlan $build_plan = null) { $this->buildPlan = $build_plan; return $this; } public function getBuildPlan() { return $this->assertAttached($this->buildPlan); } public function getBuildTargets() { return $this->assertAttached($this->buildTargets); } public function attachBuildTargets(array $targets) { $this->buildTargets = $targets; return $this; } public function isBuilding() { return $this->getBuildStatus() === self::STATUS_PENDING || $this->getBuildStatus() === self::STATUS_BUILDING; } public function isAutobuild() { return ($this->getPlanAutoKey() !== null); } public function createLog( HarbormasterBuildTarget $build_target, $log_source, $log_type) { $log_source = id(new PhutilUTF8StringTruncator()) ->setMaximumBytes(250) ->truncateString($log_source); $log = HarbormasterBuildLog::initializeNewBuildLog($build_target) ->setLogSource($log_source) ->setLogType($log_type) ->save(); return $log; } public function retrieveVariablesFromBuild() { $results = array( 'buildable.diff' => null, 'buildable.revision' => null, 'buildable.commit' => null, 'repository.callsign' => null, 'repository.phid' => null, 'repository.vcs' => null, 'repository.uri' => null, 'step.timestamp' => null, 'build.id' => null, 'initiator.phid' => null, ); foreach ($this->getBuildParameters() as $key => $value) { $results['build/'.$key] = $value; } $buildable = $this->getBuildable(); $object = $buildable->getBuildableObject(); $object_variables = $object->getBuildVariables(); $results = $object_variables + $results; $results['step.timestamp'] = time(); $results['build.id'] = $this->getID(); $results['initiator.phid'] = $this->getInitiatorPHID(); return $results; } public static function getAvailableBuildVariables() { $objects = id(new PhutilClassMapQuery()) ->setAncestorClass('HarbormasterBuildableInterface') ->execute(); $variables = array(); $variables[] = array( 'step.timestamp' => pht('The current UNIX timestamp.'), 'build.id' => pht('The ID of the current build.'), 'target.phid' => pht('The PHID of the current build target.'), 'initiator.phid' => pht( 'The PHID of the user or Object that initiated the build, '. 'if applicable.'), ); foreach ($objects as $object) { $variables[] = $object->getAvailableBuildVariables(); } $variables = array_mergev($variables); return $variables; } public function isComplete() { switch ($this->getBuildStatus()) { case self::STATUS_PASSED: case self::STATUS_FAILED: case self::STATUS_ABORTED: case self::STATUS_ERROR: case self::STATUS_PAUSED: return true; } return false; } public function isPaused() { return ($this->getBuildStatus() == self::STATUS_PAUSED); } /* -( Build Commands )----------------------------------------------------- */ private function getUnprocessedCommands() { return $this->assertAttached($this->unprocessedCommands); } public function attachUnprocessedCommands(array $commands) { $this->unprocessedCommands = $commands; return $this; } public function canRestartBuild() { if ($this->isAutobuild()) { return false; } return !$this->isRestarting(); } public function canPauseBuild() { if ($this->isAutobuild()) { return false; } return !$this->isComplete() && !$this->isPaused() && !$this->isPausing(); } public function canAbortBuild() { if ($this->isAutobuild()) { return false; } return !$this->isComplete(); } public function canResumeBuild() { if ($this->isAutobuild()) { return false; } return $this->isPaused() && !$this->isResuming(); } public function isPausing() { $is_pausing = false; foreach ($this->getUnprocessedCommands() as $command_object) { $command = $command_object->getCommand(); switch ($command) { case HarbormasterBuildCommand::COMMAND_PAUSE: $is_pausing = true; break; case HarbormasterBuildCommand::COMMAND_RESUME: case HarbormasterBuildCommand::COMMAND_RESTART: $is_pausing = false; break; case HarbormasterBuildCommand::COMMAND_ABORT: $is_pausing = true; break; } } return $is_pausing; } public function isResuming() { $is_resuming = false; foreach ($this->getUnprocessedCommands() as $command_object) { $command = $command_object->getCommand(); switch ($command) { case HarbormasterBuildCommand::COMMAND_RESTART: case HarbormasterBuildCommand::COMMAND_RESUME: $is_resuming = true; break; case HarbormasterBuildCommand::COMMAND_PAUSE: $is_resuming = false; break; case HarbormasterBuildCommand::COMMAND_ABORT: $is_resuming = false; break; } } return $is_resuming; } public function isRestarting() { $is_restarting = false; foreach ($this->getUnprocessedCommands() as $command_object) { $command = $command_object->getCommand(); switch ($command) { case HarbormasterBuildCommand::COMMAND_RESTART: $is_restarting = true; break; } } 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(); unset($this->unprocessedCommands[$key]); } return $this; } + public function canIssueCommand(PhabricatorUser $viewer, $command) { + try { + $this->assertCanIssueCommand($viewer, $command); + return true; + } catch (Exception $ex) { + return false; + } + } + + public function assertCanIssueCommand(PhabricatorUser $viewer, $command) { + $need_edit = false; + switch ($command) { + case HarbormasterBuildCommand::COMMAND_RESTART: + break; + case HarbormasterBuildCommand::COMMAND_PAUSE: + case HarbormasterBuildCommand::COMMAND_RESUME: + case HarbormasterBuildCommand::COMMAND_ABORT: + $need_edit = true; + break; + default: + throw new Exception( + pht( + 'Invalid Harbormaster build command "%s".', + $command)); + } + + // Issuing these commands requires that you be able to edit the build, to + // prevent enemy engineers from sabotaging your builds. See T9614. + if ($need_edit) { + PhabricatorPolicyFilter::requireCapability( + $viewer, + $this->getBuildPlan(), + PhabricatorPolicyCapability::CAN_EDIT); + } + } + /* -( PhabricatorApplicationTransactionInterface )------------------------- */ public function getApplicationTransactionEditor() { return new HarbormasterBuildTransactionEditor(); } public function getApplicationTransactionObject() { return $this; } public function getApplicationTransactionTemplate() { return new HarbormasterBuildTransaction(); } public function willRenderTimeline( PhabricatorApplicationTransactionView $timeline, AphrontRequest $request) { return $timeline; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, PhabricatorPolicyCapability::CAN_EDIT, ); } public function getPolicy($capability) { return $this->getBuildable()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBuildable()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('A build inherits policies from its buildable.'); } }