diff --git a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php index b5bdec1030..5ed96d8c40 100644 --- a/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php +++ b/src/applications/harbormaster/controller/HarbormasterBuildableViewController.php @@ -1,236 +1,293 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $this->id; $buildable = id(new HarbormasterBuildableQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needBuildableHandles(true) ->needContainerHandles(true) - ->needBuilds(true) ->executeOne(); if (!$buildable) { return new Aphront404Response(); } - $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); - - switch ($build->getBuildStatus()) { - case HarbormasterBuild::STATUS_INACTIVE: - $item->setBarColor('grey'); - $item->addAttribute(pht('Inactive')); - break; - case HarbormasterBuild::STATUS_PENDING: - $item->setBarColor('blue'); - $item->addAttribute(pht('Pending')); - break; - case HarbormasterBuild::STATUS_WAITING: - $item->setBarColor('violet'); - $item->addAttribute(pht('Waiting')); - break; - case HarbormasterBuild::STATUS_BUILDING: - $item->setBarColor('yellow'); - $item->addAttribute(pht('Building')); - break; - case HarbormasterBuild::STATUS_PASSED: - $item->setBarColor('green'); - $item->addAttribute(pht('Passed')); - break; - case HarbormasterBuild::STATUS_FAILED: - $item->setBarColor('red'); - $item->addAttribute(pht('Failed')); - break; - case HarbormasterBuild::STATUS_ERROR: - $item->setBarColor('red'); - $item->addAttribute(pht('Unexpected Error')); - break; - case HarbormasterBuild::STATUS_STOPPED: - $item->setBarColor('black'); - $item->addAttribute(pht('Stopped')); - break; - } - - if ($build->isRestarting()) { - $item->addIcon('backward', pht('Restarting')); - } else if ($build->isStopping()) { - $item->addIcon('stop', pht('Stopping')); - } else if ($build->isResuming()) { - $item->addIcon('play', pht('Resuming')); - } - - $build_id = $build->getID(); - - $restart_uri = "build/restart/{$build_id}/buildable/"; - $resume_uri = "build/resume/{$build_id}/buildable/"; - $stop_uri = "build/stop/{$build_id}/buildable/"; - - $item->addAction( - id(new PHUIListItemView()) - ->setIcon('backward') - ->setName(pht('Restart')) - ->setHref($this->getApplicationURI($restart_uri)) - ->setWorkflow(true) - ->setDisabled(!$build->canRestartBuild())); + // Pull builds and build targets. + $builds = id(new HarbormasterBuildQuery()) + ->setViewer($viewer) + ->withBuildablePHIDs(array($buildable->getPHID())) + ->needBuildTargets(true) + ->execute(); - if ($build->canResumeBuild()) { - $item->addAction( - id(new PHUIListItemView()) - ->setIcon('play') - ->setName(pht('Resume')) - ->setHref($this->getApplicationURI($resume_uri)) - ->setWorkflow(true)); - } else { - $item->addAction( - id(new PHUIListItemView()) - ->setIcon('stop') - ->setName(pht('Stop')) - ->setHref($this->getApplicationURI($stop_uri)) - ->setWorkflow(true) - ->setDisabled(!$build->canStopBuild())); - } + $buildable->attachBuilds($builds); - $build_list->addItem($item); - } + $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); $actions = $this->buildActionList($buildable); $this->buildPropertyLists($box, $buildable, $actions); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb("B{$id}"); return $this->buildApplicationPage( array( $crumbs, $box, $build_list, ), array( 'title' => $title, 'device' => true, )); } private function buildActionList(HarbormasterBuildable $buildable) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $buildable->getID(); $list = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($buildable) ->setObjectURI($buildable->getMonogram()); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $buildable, PhabricatorPolicyCapability::CAN_EDIT); $can_restart = false; $can_resume = false; $can_stop = false; foreach ($buildable->getBuilds() as $build) { if ($build->canRestartBuild()) { $can_restart = true; } if ($build->canResumeBuild()) { $can_resume = true; } if ($build->canStopBuild()) { $can_stop = true; } } $restart_uri = "buildable/{$id}/restart/"; $stop_uri = "buildable/{$id}/stop/"; $resume_uri = "buildable/{$id}/resume/"; $list->addAction( id(new PhabricatorActionView()) ->setIcon('backward') ->setName(pht('Restart All Builds')) ->setHref($this->getApplicationURI($restart_uri)) ->setWorkflow(true) ->setDisabled(!$can_restart || !$can_edit)); $list->addAction( id(new PhabricatorActionView()) ->setIcon('stop') ->setName(pht('Stop All Builds')) ->setHref($this->getApplicationURI($stop_uri)) ->setWorkflow(true) ->setDisabled(!$can_stop || !$can_edit)); $list->addAction( id(new PhabricatorActionView()) ->setIcon('play') ->setName(pht('Resume All Builds')) ->setHref($this->getApplicationURI($resume_uri)) ->setWorkflow(true) ->setDisabled(!$can_resume || !$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); $properties->addProperty( pht('Buildable'), $buildable->getBuildableHandle()->renderLink()); if ($buildable->getContainerHandle() !== null) { $properties->addProperty( pht('Container'), $buildable->getContainerHandle()->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); + + switch ($build->getBuildStatus()) { + case HarbormasterBuild::STATUS_INACTIVE: + $item->setBarColor('grey'); + $item->addAttribute(pht('Inactive')); + break; + case HarbormasterBuild::STATUS_PENDING: + $item->setBarColor('blue'); + $item->addAttribute(pht('Pending')); + break; + case HarbormasterBuild::STATUS_WAITING: + $item->setBarColor('violet'); + $item->addAttribute(pht('Waiting')); + break; + case HarbormasterBuild::STATUS_BUILDING: + $item->setBarColor('yellow'); + $item->addAttribute(pht('Building')); + break; + case HarbormasterBuild::STATUS_PASSED: + $item->setBarColor('green'); + $item->addAttribute(pht('Passed')); + break; + case HarbormasterBuild::STATUS_FAILED: + $item->setBarColor('red'); + $item->addAttribute(pht('Failed')); + break; + case HarbormasterBuild::STATUS_ERROR: + $item->setBarColor('red'); + $item->addAttribute(pht('Unexpected Error')); + break; + case HarbormasterBuild::STATUS_STOPPED: + $item->setBarColor('black'); + $item->addAttribute(pht('Stopped')); + break; + } + + if ($build->isRestarting()) { + $item->addIcon('backward', pht('Restarting')); + } else if ($build->isStopping()) { + $item->addIcon('stop', pht('Stopping')); + } else if ($build->isResuming()) { + $item->addIcon('play', pht('Resuming')); + } + + $build_id = $build->getID(); + + $restart_uri = "build/restart/{$build_id}/buildable/"; + $resume_uri = "build/resume/{$build_id}/buildable/"; + $stop_uri = "build/stop/{$build_id}/buildable/"; + + $item->addAction( + id(new PHUIListItemView()) + ->setIcon('backward') + ->setName(pht('Restart')) + ->setHref($this->getApplicationURI($restart_uri)) + ->setWorkflow(true) + ->setDisabled(!$build->canRestartBuild())); + + if ($build->canResumeBuild()) { + $item->addAction( + id(new PHUIListItemView()) + ->setIcon('play') + ->setName(pht('Resume')) + ->setHref($this->getApplicationURI($resume_uri)) + ->setWorkflow(true)); + } else { + $item->addAction( + id(new PHUIListItemView()) + ->setIcon('stop') + ->setName(pht('Stop')) + ->setHref($this->getApplicationURI($stop_uri)) + ->setWorkflow(true) + ->setDisabled(!$build->canStopBuild())); + } + + $targets = $build->getBuildTargets(); + + if ($targets) { + $target_list = id(new PHUIStatusListView()); + foreach ($targets as $target) { + switch ($target->getTargetStatus()) { + case HarbormasterBuildTarget::STATUS_PENDING: + $icon = 'time-green'; + break; + case HarbormasterBuildTarget::STATUS_PASSED: + $icon = 'accept-green'; + break; + case HarbormasterBuildTarget::STATUS_FAILED: + $icon = 'reject-red'; + break; + default: + $icon = 'question'; + break; + } + + try { + $impl = $target->getImplementation(); + $name = $impl->getName(); + } catch (Exception $ex) { + $name = $target->getClassName(); + } + + $target_list->addItem( + id(new PHUIStatusItemView()) + ->setIcon($icon) + ->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); + } + + return $build_list; + } + } diff --git a/src/applications/harbormaster/controller/HarbormasterStepAddController.php b/src/applications/harbormaster/controller/HarbormasterStepAddController.php index 3fe6d2c1ab..0e0eb0538d 100644 --- a/src/applications/harbormaster/controller/HarbormasterStepAddController.php +++ b/src/applications/harbormaster/controller/HarbormasterStepAddController.php @@ -1,88 +1,77 @@ id = $data['id']; } public function processRequest() { $request = $this->getRequest(); $viewer = $request->getUser(); $this->requireApplicationCapability( HarbormasterCapabilityManagePlans::CAPABILITY); $id = $this->id; $plan = id(new HarbormasterBuildPlanQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->executeOne(); - if ($plan === null) { - throw new Exception("Build plan not found!"); + if (!$plan) { + return new Aphront404Response(); } - $implementations = - HarbormasterBuildStepImplementation::getImplementations(); - $cancel_uri = $this->getApplicationURI('plan/'.$plan->getID().'/'); if ($request->isDialogFormPost()) { $class = $request->getStr('step-type'); - if (!in_array($class, $implementations)) { - return $this->createDialog($implementations, $cancel_uri); + if (!HarbormasterBuildStepImplementation::getImplementation($class)) { + return $this->createDialog($cancel_uri); } $steps = $plan->loadOrderedBuildSteps(); $step = new HarbormasterBuildStep(); $step->setBuildPlanPHID($plan->getPHID()); $step->setClassName($class); $step->setDetails(array()); $step->setSequence(count($steps) + 1); $step->save(); $edit_uri = $this->getApplicationURI("step/edit/".$step->getID()."/"); return id(new AphrontRedirectResponse())->setURI($edit_uri); } - return $this->createDialog($implementations, $cancel_uri); + return $this->createDialog($cancel_uri); } - function createDialog(array $implementations, $cancel_uri) { + private function createDialog($cancel_uri) { $request = $this->getRequest(); $viewer = $request->getUser(); $control = id(new AphrontFormRadioButtonControl()) ->setName('step-type'); - foreach ($implementations as $implementation_name) { - $implementation = new $implementation_name(); - $control - ->addButton( - $implementation_name, - $implementation->getName(), - $implementation->getGenericDescription()); + $all = HarbormasterBuildStepImplementation::getImplementations(); + foreach ($all as $class => $implementation) { + $control->addButton( + $class, + $implementation->getName(), + $implementation->getGenericDescription()); } - $dialog = new AphrontDialogView(); - $dialog->setTitle(pht('Add New Step')) - ->setUser($viewer) - ->addSubmitButton(pht('Add Build Step')) - ->addCancelButton($cancel_uri); - $dialog->appendChild( - phutil_tag( - 'p', - array(), - pht( - 'Select what type of build step you want to add: '))); - $dialog->appendChild($control); - return id(new AphrontDialogResponse())->setDialog($dialog); + return $this->newDialog() + ->setTitle(pht('Add New Step')) + ->addSubmitButton(pht('Add Build Step')) + ->addCancelButton($cancel_uri) + ->appendParagraph(pht('Choose a type of build step to add:')) + ->appendChild($control); } } diff --git a/src/applications/harbormaster/query/HarbormasterBuildQuery.php b/src/applications/harbormaster/query/HarbormasterBuildQuery.php index 930cd07b83..1fdab9cdef 100644 --- a/src/applications/harbormaster/query/HarbormasterBuildQuery.php +++ b/src/applications/harbormaster/query/HarbormasterBuildQuery.php @@ -1,155 +1,179 @@ ids = $ids; return $this; } public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } public function withBuildStatuses(array $build_statuses) { $this->buildStatuses = $build_statuses; return $this; } public function withBuildablePHIDs(array $buildable_phids) { $this->buildablePHIDs = $buildable_phids; return $this; } public function withBuildPlanPHIDs(array $build_plan_phids) { $this->buildPlanPHIDs = $build_plan_phids; return $this; } + public function needBuildTargets($need_targets) { + $this->needBuildTargets = $need_targets; + return $this; + } + protected function loadPage() { $table = new HarbormasterBuild(); $conn_r = $table->establishConnection('r'); $data = queryfx_all( $conn_r, 'SELECT * FROM %T %Q %Q %Q', $table->getTableName(), $this->buildWhereClause($conn_r), $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); return $table->loadAllFromArray($data); } protected function willFilterPage(array $page) { $buildables = array(); $buildable_phids = array_filter(mpull($page, 'getBuildablePHID')); if ($buildable_phids) { $buildables = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($buildable_phids) ->setParentQuery($this) ->execute(); $buildables = mpull($buildables, null, 'getPHID'); } foreach ($page as $key => $build) { $buildable_phid = $build->getBuildablePHID(); if (empty($buildables[$buildable_phid])) { unset($page[$key]); continue; } $build->attachBuildable($buildables[$buildable_phid]); } return $page; } protected function didFilterPage(array $page) { $plans = array(); $plan_phids = array_filter(mpull($page, 'getBuildPlanPHID')); if ($plan_phids) { $plans = id(new PhabricatorObjectQuery()) ->setViewer($this->getViewer()) ->withPHIDs($plan_phids) ->setParentQuery($this) ->execute(); $plans = mpull($plans, null, 'getPHID'); } foreach ($page as $key => $build) { $plan_phid = $build->getBuildPlanPHID(); $build->attachBuildPlan(idx($plans, $plan_phid)); } $build_phids = mpull($page, 'getPHID'); $commands = id(new HarbormasterBuildCommand())->loadAllWhere( 'targetPHID IN (%Ls) ORDER BY id ASC', $build_phids); $commands = mgroup($commands, 'getTargetPHID'); foreach ($page as $build) { $unprocessed_commands = idx($commands, $build->getPHID(), array()); $build->attachUnprocessedCommands($unprocessed_commands); } + if ($this->needBuildTargets) { + $targets = id(new HarbormasterBuildTargetQuery()) + ->setViewer($this->getViewer()) + ->setParentQuery($this) + ->withBuildPHIDs($build_phids) + ->execute(); + + // TODO: Some day, when targets have dependencies, we should toposort + // these. For now, just put them into chronological order. + $targets = array_reverse($targets); + + $targets = mgroup($targets, 'getBuildPHID'); + foreach ($page as $build) { + $build_targets = idx($targets, $build->getPHID(), array()); + $build->attachBuildTargets($build_targets); + } + } + return $page; } private function buildWhereClause(AphrontDatabaseConnection $conn_r) { $where = array(); if ($this->ids) { $where[] = qsprintf( $conn_r, 'id IN (%Ld)', $this->ids); } if ($this->phids) { $where[] = qsprintf( $conn_r, 'phid in (%Ls)', $this->phids); } if ($this->buildStatuses) { $where[] = qsprintf( $conn_r, 'buildStatus in (%Ls)', $this->buildStatuses); } if ($this->buildablePHIDs) { $where[] = qsprintf( $conn_r, 'buildablePHID IN (%Ls)', $this->buildablePHIDs); } if ($this->buildPlanPHIDs) { $where[] = qsprintf( $conn_r, 'buildPlanPHID IN (%Ls)', $this->buildPlanPHIDs); } $where[] = $this->buildPagingClause($conn_r); return $this->formatWhereClause($where); } public function getQueryApplicationClass() { return 'PhabricatorApplicationHarbormaster'; } } diff --git a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php index 5d47aee406..4ba72c09b7 100644 --- a/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterBuildStepImplementation.php @@ -1,166 +1,199 @@ setAncestorClass('HarbormasterBuildStepImplementation') - ->setConcreteOnly(true) - ->selectAndLoadSymbols(); - return ipull($symbols, 'name'); + ->loadObjects(); + } + + public static function getImplementation($class) { + $base = idx(self::getImplementations(), $class); + + if ($base) { + return (clone $base); + } + + return null; + } + + public static function requireImplementation($class) { + if (!$class) { + throw new Exception(pht('No implementation is specified!')); + } + + $implementation = self::getImplementation($class); + if (!$implementation) { + throw new Exception(pht('No such implementation "%s" exists!', $class)); + } + + return $implementation; } /** * The name of the implementation. */ abstract public function getName(); /** * The generic description of the implementation. */ public function getGenericDescription() { return ''; } /** * The description of the implementation, based on the current settings. */ public function getDescription() { return $this->getGenericDescription(); } /** * Run the build target against the specified build. */ abstract public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target); /** * Gets the settings for this build step. */ public function getSettings() { return $this->settings; } public function getSetting($key, $default = null) { return idx($this->settings, $key, $default); } /** * Loads the settings for this build step implementation from a build * step or target. */ public final function loadSettings($build_object) { $this->settings = $build_object->getDetails(); return $this; } /** * Return the name of artifacts produced by this command. * * Something like: * * return array( * 'some_name_input_by_user' => 'host'); * * Future steps will calculate all available artifact mappings * before them and filter on the type. * * @return array The mappings of artifact names to their types. */ public function getArtifactInputs() { return array(); } public function getArtifactOutputs() { return array(); } /** * Returns a list of all artifacts made available by previous build steps. */ public static function loadAvailableArtifacts( HarbormasterBuildPlan $build_plan, HarbormasterBuildStep $current_build_step, $artifact_type) { $build_steps = $build_plan->loadOrderedBuildSteps(); return self::getAvailableArtifacts( $build_plan, $build_steps, $current_build_step, $artifact_type); } /** * Returns a list of all artifacts made available by previous build steps. */ public static function getAvailableArtifacts( HarbormasterBuildPlan $build_plan, array $build_steps, HarbormasterBuildStep $current_build_step, $artifact_type) { $previous_implementations = array(); foreach ($build_steps as $build_step) { if ($build_step->getPHID() === $current_build_step->getPHID()) { break; } $previous_implementations[] = $build_step->getStepImplementation(); } $artifact_arrays = mpull($previous_implementations, 'getArtifactOutputs'); $artifacts = array(); foreach ($artifact_arrays as $array) { $array = ipull($array, 'type', 'key'); foreach ($array as $name => $type) { if ($type !== $artifact_type && $artifact_type !== null) { continue; } $artifacts[$name] = $type; } } return $artifacts; } /** * Convert a user-provided string with variables in it, like: * * ls ${dirname} * * ...into a string with variables merged into it safely: * * ls 'dir with spaces' * * @param string Name of a `vxsprintf` function, like @{function:vcsprintf}. * @param string User-provided pattern string containing `${variables}`. * @param dict List of available replacement variables. * @return string String with variables replaced safely into it. */ protected function mergeVariables($function, $pattern, array $variables) { $regexp = '/\\$\\{(?P[a-z\\.]+)\\}/'; $matches = null; preg_match_all($regexp, $pattern, $matches); $argv = array(); foreach ($matches['name'] as $name) { if (!array_key_exists($name, $variables)) { throw new Exception(pht("No such variable '%s'!", $name)); } $argv[] = $variables[$name]; } $pattern = str_replace('%', '%%', $pattern); $pattern = preg_replace($regexp, '%s', $pattern); return call_user_func($function, $pattern, $argv); } public function getFieldSpecifications() { return array(); } + protected function formatSettingForDescription($key, $default = null) { + return $this->formatValueForDescription($this->getSetting($key, $default)); + } + + protected function formatValueForDescription($value) { + if (strlen($value)) { + return phutil_tag('strong', array(), $value); + } else { + return phutil_tag('em', array(), pht('(null)')); + } + } + } diff --git a/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php index d21ba09a1e..a0d481b63b 100644 --- a/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterCommandBuildStepImplementation.php @@ -1,102 +1,100 @@ getSettings(); - return pht( - 'Run \'%s\' on \'%s\'.', - $settings['command'], - $settings['hostartifact']); + 'Run command %s on host %s.', + $this->formatSettingForDescription('command'), + $this->formatSettingForDescription('hostartifact')); } public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { $settings = $this->getSettings(); $variables = $build_target->getVariables(); $command = $this->mergeVariables( 'vcsprintf', $settings['command'], $variables); $artifact = $build->loadArtifact($settings['hostartifact']); $lease = $artifact->loadDrydockLease(); $interface = $lease->getInterface('command'); $future = $interface->getExecFuture('%C', $command); $log_stdout = $build->createLog($build_target, "remote", "stdout"); $log_stderr = $build->createLog($build_target, "remote", "stderr"); $start_stdout = $log_stdout->start(); $start_stderr = $log_stderr->start(); // Read the next amount of available output every second. while (!$future->isReady()) { list($stdout, $stderr) = $future->read(); $log_stdout->append($stdout); $log_stderr->append($stderr); $future->discardBuffers(); // Wait one second before querying for more data. sleep(1); } // Get the return value so we can log that as well. list($err) = $future->resolve(); // Retrieve the last few bits of information. list($stdout, $stderr) = $future->read(); $log_stdout->append($stdout); $log_stderr->append($stderr); $future->discardBuffers(); $log_stdout->finalize($start_stdout); $log_stderr->finalize($start_stderr); if ($err) { throw new Exception(pht('Command failed with error %d.', $err)); } } public function getArtifactInputs() { return array( array( 'name' => pht('Run on Host'), 'key' => $this->getSetting('hostartifact'), 'type' => HarbormasterBuildArtifact::TYPE_HOST, ), ); } public function getFieldSpecifications() { return array( 'command' => array( 'name' => pht('Command'), 'type' => 'text', 'required' => true, ), 'hostartifact' => array( 'name' => pht('Host'), 'type' => 'text', 'required' => true, ), ); } } diff --git a/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php index b34ea35608..5af731d81f 100644 --- a/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterHTTPRequestBuildStepImplementation.php @@ -1,67 +1,72 @@ getSettings(); + $domain = null; + $uri = $this->getSetting('uri'); + if ($uri) { + $domain = id(new PhutilURI($uri))->getDomain(); + } - $uri = new PhutilURI($settings['uri']); - $domain = $uri->getDomain(); - return pht('Make an HTTP %s request to %s', $settings['method'], $domain); + return pht( + 'Make an HTTP %s request to %s.', + $this->formatSettingForDescription('method', 'POST'), + $this->formatValueForDescription($domain)); } public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { $settings = $this->getSettings(); $variables = $build_target->getVariables(); $uri = $this->mergeVariables( 'vurisprintf', $settings['uri'], $variables); $log_body = $build->createLog($build_target, $uri, 'http-body'); $start = $log_body->start(); $method = nonempty(idx($settings, 'method'), 'POST'); list($status, $body, $headers) = id(new HTTPSFuture($uri)) ->setMethod($method) ->setTimeout(60) ->resolve(); $log_body->append($body); $log_body->finalize($start); if ($status->getStatusCode() != 200) { $build->setBuildStatus(HarbormasterBuild::STATUS_FAILED); } } public function getFieldSpecifications() { return array( 'uri' => array( 'name' => pht('URI'), 'type' => 'text', 'required' => true, ), 'method' => array( 'name' => pht('HTTP Method'), 'type' => 'select', 'options' => array_fuse(array('POST', 'GET', 'PUT', 'DELETE')), ), ); } } diff --git a/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php index a42f3ae079..90efde27ec 100644 --- a/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterLeaseHostBuildStepImplementation.php @@ -1,79 +1,69 @@ getSettings(); - - return pht( - 'Obtain a lease on a Drydock host whose platform is \'%s\' and store '. - 'the resulting lease in a host artifact called \'%s\'.', - $settings['platform'], - $settings['name']); - } - public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { $settings = $this->getSettings(); // Create the lease. $lease = id(new DrydockLease()) ->setResourceType('host') ->setAttributes( array( 'platform' => $settings['platform'], )) ->queueForActivation(); // Wait until the lease is fulfilled. // TODO: This will throw an exception if the lease can't be fulfilled; // we should treat that as build failure not build error. $lease->waitUntilActive(); // Create the associated artifact. $artifact = $build->createArtifact( $build_target, $settings['name'], HarbormasterBuildArtifact::TYPE_HOST); $artifact->setArtifactData(array( 'drydock-lease' => $lease->getID())); $artifact->save(); } public function getArtifactOutputs() { return array( array( 'name' => pht('Leased Host'), 'key' => $this->getSetting('name'), 'type' => HarbormasterBuildArtifact::TYPE_HOST, ), ); } public function getFieldSpecifications() { return array( 'name' => array( 'name' => pht('Artifact Name'), 'type' => 'text', 'required' => true, ), 'platform' => array( 'name' => pht('Platform'), 'type' => 'text', 'required' => true, ), ); } } diff --git a/src/applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php index e1dda2bff8..be007cad0f 100644 --- a/src/applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterPublishFragmentBuildStepImplementation.php @@ -1,85 +1,83 @@ getSettings(); - return pht( - 'Publish file artifact \'%s\' to the fragment path \'%s\'.', - $settings['artifact'], - $settings['path']); + 'Publish file artifact %s as fragment %s.', + $this->formatSettingForDescription('artifact'), + $this->formatSettingForDescription('path')); } public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { $settings = $this->getSettings(); $variables = $build_target->getVariables(); $path = $this->mergeVariables( 'vsprintf', $settings['path'], $variables); $artifact = $build->loadArtifact($settings['artifact']); $file = $artifact->loadPhabricatorFile(); $fragment = id(new PhragmentFragmentQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPaths(array($path)) ->executeOne(); if ($fragment === null) { PhragmentFragment::createFromFile( PhabricatorUser::getOmnipotentUser(), $file, $path, PhabricatorPolicies::getMostOpenPolicy(), PhabricatorPolicies::POLICY_USER); } else { if ($file->getMimeType() === "application/zip") { $fragment->updateFromZIP(PhabricatorUser::getOmnipotentUser(), $file); } else { $fragment->updateFromFile(PhabricatorUser::getOmnipotentUser(), $file); } } } public function getArtifactInputs() { return array( array( 'name' => pht('Publishes File'), 'key' => $this->getSetting('artifact'), 'type' => HarbormasterBuildArtifact::TYPE_FILE, ), ); } public function getFieldSpecifications() { return array( 'path' => array( 'name' => pht('Path'), 'type' => 'text', 'required' => true, ), 'artifact' => array( 'name' => pht('File Artifact'), 'type' => 'text', 'required' => true, ), ); } } diff --git a/src/applications/harbormaster/step/HarbormasterSleepBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterSleepBuildStepImplementation.php index 35571f216f..bca222c17a 100644 --- a/src/applications/harbormaster/step/HarbormasterSleepBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterSleepBuildStepImplementation.php @@ -1,40 +1,40 @@ getSettings(); - - return pht('Sleep for %s seconds.', $settings['seconds']); + return pht( + 'Sleep for %s seconds.', + $this->formatSettingForDescription('seconds')); } public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { $settings = $this->getSettings(); sleep($settings['seconds']); } public function getFieldSpecifications() { return array( 'seconds' => array( 'name' => pht('Seconds'), 'type' => 'int', 'required' => true, 'caption' => pht('The number of seconds to sleep for.'), ), ); } } diff --git a/src/applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php b/src/applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php index 0d3985f292..402a03ba20 100644 --- a/src/applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php +++ b/src/applications/harbormaster/step/HarbormasterThrowExceptionBuildStep.php @@ -1,25 +1,21 @@ getSettings(); - return pht( - 'Upload artifact located at \'%s\' on \'%s\'.', - $settings['path'], - $settings['hostartifact']); + 'Upload %s from %s.', + $this->formatSettingForDescription('path'), + $this->formatSettingForDescription('hostartifact')); } public function execute( HarbormasterBuild $build, HarbormasterBuildTarget $build_target) { $settings = $this->getSettings(); $variables = $build_target->getVariables(); $path = $this->mergeVariables( 'vsprintf', $settings['path'], $variables); $artifact = $build->loadArtifact($settings['hostartifact']); $lease = $artifact->loadDrydockLease(); $interface = $lease->getInterface('filesystem'); // TODO: Handle exceptions. $file = $interface->saveFile($path, $settings['name']); // Insert the artifact record. $artifact = $build->createArtifact( $build_target, $settings['name'], HarbormasterBuildArtifact::TYPE_FILE); $artifact->setArtifactData(array( 'filePHID' => $file->getPHID())); $artifact->save(); } public function getArtifactInputs() { return array( array( 'name' => pht('Upload From Host'), 'key' => $this->getSetting('hostartifact'), 'type' => HarbormasterBuildArtifact::TYPE_HOST, ), ); } public function getArtifactOutputs() { return array( array( 'name' => pht('Uploaded File'), 'key' => $this->getSetting('name'), 'type' => HarbormasterBuildArtifact::TYPE_FILE, ), ); } public function getFieldSpecifications() { return array( 'path' => array( 'name' => pht('Path'), 'type' => 'text', 'required' => true, ), 'name' => array( 'name' => pht('Local Name'), 'type' => 'text', 'required' => true, ), 'hostartifact' => array( 'name' => pht('Host Artifact'), 'type' => 'text', 'required' => true, ), ); } } diff --git a/src/applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php b/src/applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php index 1c802f0b85..f1d7481f3e 100644 --- a/src/applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php +++ b/src/applications/harbormaster/step/HarbormasterWaitForPreviousBuildStepImplementation.php @@ -1,110 +1,104 @@ getBuildable(); $object = $buildable->getBuildableObject(); if (!($object instanceof PhabricatorRepositoryCommit)) { return; } // Block until all previous builds of the same build plan have // finished. $plan = $build->getBuildPlan(); $log = null; $log_start = null; $blockers = $this->getBlockers($object, $plan, $build); while (count($blockers) > 0) { if ($log === null) { $log = $build->createLog($build_target, "waiting", "blockers"); $log_start = $log->start(); } $log->append("Blocked by: ".implode(",", $blockers)."\n"); // TODO: This should fail temporarily instead after setting the target to // waiting, and thereby push the build into a waiting status. sleep(1); $blockers = $this->getBlockers($object, $plan, $build); } if ($log !== null) { $log->finalize($log_start); } } private function getBlockers( PhabricatorRepositoryCommit $commit, HarbormasterBuildPlan $plan, HarbormasterBuild $source) { $call = new ConduitCall( 'diffusion.commitparentsquery', array( 'commit' => $commit->getCommitIdentifier(), 'callsign' => $commit->getRepository()->getCallsign() )); $call->setUser(PhabricatorUser::getOmnipotentUser()); $parents = $call->execute(); $parents = id(new DiffusionCommitQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withRepository($commit->getRepository()) ->withIdentifiers($parents) ->execute(); $blockers = array(); $build_objects = array(); foreach ($parents as $parent) { if (!$parent->isImported()) { $blockers[] = pht('Commit %s', $parent->getCommitIdentifier()); } else { $build_objects[] = $parent->getPHID(); } } $buildables = id(new HarbormasterBuildableQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuildablePHIDs($build_objects) ->withManualBuildables(false) ->execute(); $buildable_phids = mpull($buildables, 'getPHID'); $builds = id(new HarbormasterBuildQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withBuildablePHIDs($buildable_phids) ->withBuildPlanPHIDs(array($plan->getPHID())) ->execute(); foreach ($builds as $build) { if (!$build->isComplete()) { $blockers[] = pht('Build %d', $build->getID()); } } return $blockers; } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuild.php b/src/applications/harbormaster/storage/build/HarbormasterBuild.php index fe24542853..be26c7238d 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -1,334 +1,344 @@ setBuildStatus(self::STATUS_INACTIVE); } public function delete() { $this->openTransaction(); $this->deleteUnprocessedCommands(); $result = parent::delete(); $this->saveTransaction(); return $result; } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterPHIDTypeBuild::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_WAITING || $this->getBuildStatus() === self::STATUS_BUILDING; } public function createLog( HarbormasterBuildTarget $build_target, $log_source, $log_type) { $log_source = phutil_utf8_shorten($log_source, 250); $log = HarbormasterBuildLog::initializeNewBuildLog($build_target) ->setLogSource($log_source) ->setLogType($log_type) ->save(); return $log; } public function createArtifact( HarbormasterBuildTarget $build_target, $artifact_key, $artifact_type) { $artifact = HarbormasterBuildArtifact::initializeNewBuildArtifact($build_target); $artifact->setArtifactKey($this->getPHID(), $artifact_key); $artifact->setArtifactType($artifact_type); $artifact->save(); return $artifact; } public function loadArtifact($name) { $artifact = id(new HarbormasterBuildArtifactQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withArtifactKeys( $this->getPHID(), array($name)) ->executeOne(); if ($artifact === null) { throw new Exception("Artifact not found!"); } return $artifact; } public function retrieveVariablesFromBuild() { $results = array( 'buildable.diff' => null, 'buildable.revision' => null, 'buildable.commit' => null, 'repository.callsign' => null, 'repository.vcs' => null, 'repository.uri' => null, 'step.timestamp' => null, 'build.id' => null); $buildable = $this->getBuildable(); $object = $buildable->getBuildableObject(); $repo = null; if ($object instanceof DifferentialDiff) { $results['buildable.diff'] = $object->getID(); $revision = $object->getRevision(); $results['buildable.revision'] = $revision->getID(); $repo = $revision->getRepository(); } else if ($object instanceof PhabricatorRepositoryCommit) { $results['buildable.commit'] = $object->getCommitIdentifier(); $repo = $object->getRepository(); } if ($repo) { $results['repository.callsign'] = $repo->getCallsign(); $results['repository.vcs'] = $repo->getVersionControlSystem(); $results['repository.uri'] = $repo->getPublicCloneURI(); } $results['step.timestamp'] = time(); $results['build.id'] = $this->getID(); return $results; } public static function getAvailableBuildVariables() { return array( 'buildable.diff' => pht('The differential diff ID, if applicable.'), 'buildable.revision' => pht('The differential revision ID, if applicable.'), 'buildable.commit' => pht('The commit identifier, if applicable.'), 'repository.callsign' => pht('The callsign of the repository in Phabricator.'), 'repository.vcs' => pht('The version control system, either "svn", "hg" or "git".'), 'repository.uri' => pht('The URI to clone or checkout the repository from.'), 'step.timestamp' => pht('The current UNIX timestamp.'), 'build.id' => pht('The ID of the current build.')); } public function isComplete() { switch ($this->getBuildStatus()) { case self::STATUS_PASSED: case self::STATUS_FAILED: case self::STATUS_ERROR: case self::STATUS_STOPPED: return true; } return false; } public function isStopped() { return ($this->getBuildStatus() == self::STATUS_STOPPED); } /* -( Build Commands )----------------------------------------------------- */ private function getUnprocessedCommands() { return $this->assertAttached($this->unprocessedCommands); } public function attachUnprocessedCommands(array $commands) { $this->unprocessedCommands = $commands; return $this; } public function canRestartBuild() { return !$this->isRestarting(); } public function canStopBuild() { return !$this->isComplete() && !$this->isStopped() && !$this->isStopping(); } public function canResumeBuild() { return $this->isStopped() && !$this->isResuming(); } public function isStopping() { $is_stopping = false; foreach ($this->getUnprocessedCommands() as $command_object) { $command = $command_object->getCommand(); switch ($command) { case HarbormasterBuildCommand::COMMAND_STOP: $is_stopping = true; break; case HarbormasterBuildCommand::COMMAND_RESUME: case HarbormasterBuildCommand::COMMAND_RESTART: $is_stopping = false; break; } } return $is_stopping; } 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_STOP: $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 deleteUnprocessedCommands() { foreach ($this->getUnprocessedCommands() as $key => $command_object) { $command_object->delete(); unset($this->unprocessedCommands[$key]); } return $this; } /* -( 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.'); } } diff --git a/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php b/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php index 14cbc267da..a6a94d2abd 100644 --- a/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php +++ b/src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php @@ -1,154 +1,144 @@ setBuildPHID($build->getPHID()) ->setBuildStepPHID($build_step->getPHID()) ->setClassName($build_step->getClassName()) ->setDetails($build_step->getDetails()) ->setTargetStatus(self::STATUS_PENDING) ->setVariables($variables); } public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, 'variables' => self::SERIALIZATION_JSON, ) ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterPHIDTypeBuildTarget::TYPECONST); } public function attachBuild(HarbormasterBuild $build) { $this->build = $build; return $this; } public function getBuild() { return $this->assertAttached($this->build); } public function attachBuildStep(HarbormasterBuildStep $step) { $this->buildStep = $step; return $this; } public function getBuildStep() { return $this->assertAttached($this->buildStep); } public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function getVariable($key, $default = null) { return idx($this->variables, $key, $default); } public function setVariable($key, $value) { $this->variables[$key] = $value; return $this; } public function getImplementation() { - if ($this->className === null) { - throw new Exception("No implementation set for the given target."); + if ($this->implementation === null) { + $obj = HarbormasterBuildStepImplementation::requireImplementation( + $this->className); + $obj->loadSettings($this); + $this->implementation = $obj; } - static $implementations = null; - if ($implementations === null) { - $implementations = - HarbormasterBuildStepImplementation::getImplementations(); - } - - $class = $this->className; - if (!in_array($class, $implementations)) { - throw new Exception( - "Class name '".$class."' does not extend BuildStepImplementation."); - } - $implementation = newv($class, array()); - $implementation->loadSettings($this); - return $implementation; + return $this->implementation; } /* -( Status )------------------------------------------------------------- */ public function isComplete() { switch ($this->getTargetStatus()) { case self::STATUS_PASSED: case self::STATUS_FAILED: return true; } return false; } public function isFailed() { switch ($this->getTargetStatus()) { case self::STATUS_FAILED: return true; } return false; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->getBuild()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBuild()->hasAutomaticCapability( $capability, $viewer); } public function describeAutomaticCapability($capability) { - return pht( - 'Users must be able to see a build to view its build targets.'); + return pht('Users must be able to see a build to view its build targets.'); } } diff --git a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php index 0798b31b10..f76dc67327 100644 --- a/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php +++ b/src/applications/harbormaster/storage/configuration/HarbormasterBuildStep.php @@ -1,111 +1,102 @@ true, self::CONFIG_SERIALIZATION => array( 'details' => self::SERIALIZATION_JSON, ) ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( HarbormasterPHIDTypeBuildStep::TYPECONST); } public function attachBuildPlan(HarbormasterBuildPlan $plan) { $this->buildPlan = $plan; return $this; } public function getBuildPlan() { return $this->assertAttached($this->buildPlan); } public function getDetail($key, $default = null) { return idx($this->details, $key, $default); } public function setDetail($key, $value) { $this->details[$key] = $value; return $this; } public function getStepImplementation() { - if ($this->className === null) { - throw new Exception("No implementation set for the given step."); + if ($this->implementation === null) { + $obj = HarbormasterBuildStepImplementation::requireImplementation( + $this->className); + $obj->loadSettings($this); + $this->implementation = $obj; } - static $implementations = null; - if ($implementations === null) { - $implementations = - HarbormasterBuildStepImplementation::getImplementations(); - } - - $class = $this->className; - if (!in_array($class, $implementations)) { - throw new Exception( - "Class name '".$class."' does not extend BuildStepImplementation."); - } - $implementation = newv($class, array()); - $implementation->loadSettings($this); - return $implementation; + return $this->implementation; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { return $this->getBuildPlan()->getPolicy($capability); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return $this->getBuildPlan()->hasAutomaticCapability($capability, $viewer); } public function describeAutomaticCapability($capability) { return pht('A build step has the same policies as its build plan.'); } /* -( PhabricatorCustomFieldInterface )------------------------------------ */ public function getCustomFieldSpecificationForRole($role) { return array(); } public function getCustomFieldBaseClass() { return 'HarbormasterBuildStepCustomField'; } public function getCustomFields() { return $this->assertAttached($this->customFields); } public function attachCustomFields(PhabricatorCustomFieldAttachment $fields) { $this->customFields = $fields; return $this; } }