@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Allow external systems to send messages to build targets

Summary:
Ref T1049. Allows external systems to send a message to a build target. The primary intended use case is:

- You make an HTTP request to Jenkins.
- The build goes into a "waiting" state.
- Later, Jenkins calls `harbormaster.sendmessage` to report that the target passed or failed.
- The build continues as appropriate.

This is deceptively complicated because:

- There are a lot of race concerns. We might get a message back from an external system before it even responds to the request we made. We want to make sure we process these messages no matter when we receive them.
- These messages need to be sent to a build target (vs a build or buildable) because we'll get into trouble with parallelization later on otherwise (Jenkins is told to do 3 builds; we can't tell which ones failed or what overall state is unless the message are sent to targets).
- I initially thought about implementing this as a separate "Wait for a response from an external system" build step. This gets a lot more complicated for users once we do parallelization, though. Particularly, in the case where you've told Jenkins to do 3 builds, the three "wait" steps need to know which target they're waiting for (and jenkins needs to know some unique identifier for each target). So this pretty much boils down to a more complicated, more error-prone version of using target PHIDs.

This makes the already-muddy Build UI a bit worse, but it needs a general clarity pass anyway (it's showing way too much uninteresting data, and should show a better summary of results instead).

Test Plan:
- This doesn't really do anything interesting yet.
- Used Conduit to send messages to build plans.
- Viewed the messages on the build screen.

Reviewers: btrahan

Reviewed By: btrahan

Subscribers: epriestley

Maniphest Tasks: T1049

Differential Revision: https://secure.phabricator.com/D8604

+325 -2
+10
resources/sql/autopatches/20140323.harbor.2.message.sql
··· 1 + CREATE TABLE {$NAMESPACE}_harbormaster.harbormaster_buildmessage ( 2 + id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, 3 + authorPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, 4 + buildTargetPHID VARCHAR(64) NOT NULL COLLATE utf8_bin, 5 + type VARCHAR(16) NOT NULL, 6 + isConsumed BOOL NOT NULL, 7 + dateCreated INT UNSIGNED NOT NULL, 8 + dateModified INT UNSIGNED NOT NULL, 9 + KEY `key_buildtarget` (buildTargetPHID) 10 + ) ENGINE=InnoDB, COLLATE utf8_general_ci;
+12
src/__phutil_library_map__.php
··· 189 189 'ConduitAPI_flag_delete_Method' => 'applications/flag/conduit/ConduitAPI_flag_delete_Method.php', 190 190 'ConduitAPI_flag_edit_Method' => 'applications/flag/conduit/ConduitAPI_flag_edit_Method.php', 191 191 'ConduitAPI_flag_query_Method' => 'applications/flag/conduit/ConduitAPI_flag_query_Method.php', 192 + 'ConduitAPI_harbormaster_Method' => 'applications/harbormaster/conduit/ConduitAPI_harbormaster_Method.php', 193 + 'ConduitAPI_harbormaster_sendmessage_Method' => 'applications/harbormaster/conduit/ConduitAPI_harbormaster_sendmessage_Method.php', 192 194 'ConduitAPI_macro_Method' => 'applications/macro/conduit/ConduitAPI_macro_Method.php', 193 195 'ConduitAPI_macro_creatememe_Method' => 'applications/macro/conduit/ConduitAPI_macro_creatememe_Method.php', 194 196 'ConduitAPI_macro_query_Method' => 'applications/macro/conduit/ConduitAPI_macro_query_Method.php', ··· 701 703 'HarbormasterBuildItemQuery' => 'applications/harbormaster/query/HarbormasterBuildItemQuery.php', 702 704 'HarbormasterBuildLog' => 'applications/harbormaster/storage/build/HarbormasterBuildLog.php', 703 705 'HarbormasterBuildLogQuery' => 'applications/harbormaster/query/HarbormasterBuildLogQuery.php', 706 + 'HarbormasterBuildMessage' => 'applications/harbormaster/storage/HarbormasterBuildMessage.php', 707 + 'HarbormasterBuildMessageQuery' => 'applications/harbormaster/query/HarbormasterBuildMessageQuery.php', 704 708 'HarbormasterBuildPlan' => 'applications/harbormaster/storage/configuration/HarbormasterBuildPlan.php', 705 709 'HarbormasterBuildPlanEditor' => 'applications/harbormaster/editor/HarbormasterBuildPlanEditor.php', 706 710 'HarbormasterBuildPlanQuery' => 'applications/harbormaster/query/HarbormasterBuildPlanQuery.php', ··· 2743 2747 'ConduitAPI_flag_delete_Method' => 'ConduitAPI_flag_Method', 2744 2748 'ConduitAPI_flag_edit_Method' => 'ConduitAPI_flag_Method', 2745 2749 'ConduitAPI_flag_query_Method' => 'ConduitAPI_flag_Method', 2750 + 'ConduitAPI_harbormaster_Method' => 'ConduitAPIMethod', 2751 + 'ConduitAPI_harbormaster_sendmessage_Method' => 'ConduitAPI_harbormaster_Method', 2746 2752 'ConduitAPI_macro_Method' => 'ConduitAPIMethod', 2747 2753 'ConduitAPI_macro_creatememe_Method' => 'ConduitAPI_macro_Method', 2748 2754 'ConduitAPI_macro_query_Method' => 'ConduitAPI_macro_Method', ··· 3297 3303 1 => 'PhabricatorPolicyInterface', 3298 3304 ), 3299 3305 'HarbormasterBuildLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 3306 + 'HarbormasterBuildMessage' => 3307 + array( 3308 + 0 => 'HarbormasterDAO', 3309 + 1 => 'PhabricatorPolicyInterface', 3310 + ), 3311 + 'HarbormasterBuildMessageQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 3300 3312 'HarbormasterBuildPlan' => 3301 3313 array( 3302 3314 0 => 'HarbormasterDAO',
+18
src/applications/harbormaster/conduit/ConduitAPI_harbormaster_Method.php
··· 1 + <?php 2 + 3 + abstract class ConduitAPI_harbormaster_Method extends ConduitAPIMethod { 4 + 5 + public function getApplication() { 6 + return PhabricatorApplication::getByClass( 7 + 'PhabricatorApplicationHarbormaster'); 8 + } 9 + 10 + public function getMethodStatus() { 11 + return self::METHOD_STATUS_UNSTABLE; 12 + } 13 + 14 + public function getMethodStatusDescription() { 15 + return pht('All Harbormaster APIs are new and subject to change.'); 16 + } 17 + 18 + }
+49
src/applications/harbormaster/conduit/ConduitAPI_harbormaster_sendmessage_Method.php
··· 1 + <?php 2 + 3 + final class ConduitAPI_harbormaster_sendmessage_Method 4 + extends ConduitAPI_harbormaster_Method { 5 + 6 + public function getMethodDescription() { 7 + return pht( 8 + 'Send a message to a build target, notifying it of results in an '. 9 + 'external system.'); 10 + } 11 + 12 + public function defineParamTypes() { 13 + return array( 14 + 'buildTargetPHID' => 'phid', 15 + 'type' => 'enum<pass, fail>', 16 + ); 17 + } 18 + 19 + public function defineReturnType() { 20 + return 'void'; 21 + } 22 + 23 + public function defineErrorTypes() { 24 + return array(); 25 + } 26 + 27 + protected function execute(ConduitAPIRequest $request) { 28 + $viewer = $request->getUser(); 29 + 30 + $build_target_phid = $request->getValue('buildTargetPHID'); 31 + $message_type = $request->getValue('type'); 32 + 33 + $build_target = id(new HarbormasterBuildTargetQuery()) 34 + ->setViewer($viewer) 35 + ->withPHIDs(array($build_target_phid)) 36 + ->executeOne(); 37 + if (!$build_target) { 38 + throw new Exception(pht('No such build target!')); 39 + } 40 + 41 + $message = HarbormasterBuildMessage::initializeNewMessage($viewer) 42 + ->setBuildTargetPHID($build_target->getPHID()) 43 + ->setType($message_type) 44 + ->save(); 45 + 46 + return null; 47 + } 48 + 49 + }
+65
src/applications/harbormaster/controller/HarbormasterBuildViewController.php
··· 55 55 ->withBuildPHIDs(array($build->getPHID())) 56 56 ->execute(); 57 57 58 + 59 + if ($build_targets) { 60 + $messages = id(new HarbormasterBuildMessageQuery()) 61 + ->setViewer($viewer) 62 + ->withBuildTargetPHIDs(mpull($build_targets, 'getPHID')) 63 + ->execute(); 64 + $messages = mgroup($messages, 'getBuildTargetPHID'); 65 + } else { 66 + $messages = array(); 67 + } 68 + 58 69 $targets = array(); 59 70 foreach ($build_targets as $build_target) { 60 71 $header = id(new PHUIHeaderView()) ··· 84 95 $targets[] = id(new PHUIObjectBoxView()) 85 96 ->setHeader($header) 86 97 ->addPropertyList($properties); 98 + 99 + $build_messages = idx($messages, $build_target->getPHID(), array()); 100 + $targets[] = $this->buildMessages($build_messages); 87 101 88 102 $targets[] = $this->buildArtifacts($build_target); 89 103 $targets[] = $this->buildLog($build, $build_target); ··· 315 329 return pht('Unknown'); 316 330 } 317 331 } 332 + 333 + private function buildMessages(array $messages) { 334 + $viewer = $this->getRequest()->getUser(); 335 + 336 + if ($messages) { 337 + $handles = id(new PhabricatorHandleQuery()) 338 + ->setViewer($viewer) 339 + ->withPHIDs(mpull($messages, 'getAuthorPHID')) 340 + ->execute(); 341 + } else { 342 + $handles = array(); 343 + } 344 + 345 + $rows = array(); 346 + foreach ($messages as $message) { 347 + $rows[] = array( 348 + $message->getID(), 349 + $handles[$message->getAuthorPHID()]->renderLink(), 350 + $message->getType(), 351 + $message->getIsConsumed() ? pht('Consumed') : null, 352 + phabricator_datetime($message->getDateCreated(), $viewer), 353 + ); 354 + } 355 + 356 + $table = new AphrontTableView($rows); 357 + $table->setNoDataString(pht('No messages for this build target.')); 358 + $table->setHeaders( 359 + array( 360 + pht('ID'), 361 + pht('From'), 362 + pht('Type'), 363 + pht('Consumed'), 364 + pht('Received'), 365 + )); 366 + $table->setColumnClasses( 367 + array( 368 + '', 369 + '', 370 + 'wide', 371 + '', 372 + 'date', 373 + )); 374 + 375 + $box = id(new PHUIObjectBoxView()) 376 + ->setHeaderText(pht('Build Target Messages')) 377 + ->appendChild($table); 378 + 379 + return $box; 380 + } 381 + 382 + 318 383 319 384 }
+98
src/applications/harbormaster/query/HarbormasterBuildMessageQuery.php
··· 1 + <?php 2 + 3 + final class HarbormasterBuildMessageQuery 4 + extends PhabricatorCursorPagedPolicyAwareQuery { 5 + 6 + private $ids; 7 + private $buildTargetPHIDs; 8 + private $consumed; 9 + 10 + public function withIDs(array $ids) { 11 + $this->ids = $ids; 12 + return $this; 13 + } 14 + 15 + public function withBuildTargetPHIDs(array $phids) { 16 + $this->buildTargetPHIDs = $phids; 17 + return $this; 18 + } 19 + 20 + public function withConsumed($consumed) { 21 + $this->consumed = $consumed; 22 + return $this; 23 + } 24 + 25 + protected function loadPage() { 26 + $table = new HarbormasterBuildMessage(); 27 + $conn_r = $table->establishConnection('r'); 28 + 29 + $data = queryfx_all( 30 + $conn_r, 31 + 'SELECT * FROM %T %Q %Q %Q', 32 + $table->getTableName(), 33 + $this->buildWhereClause($conn_r), 34 + $this->buildOrderClause($conn_r), 35 + $this->buildLimitClause($conn_r)); 36 + 37 + return $table->loadAllFromArray($data); 38 + } 39 + 40 + protected function willFilterPage(array $page) { 41 + $build_target_phids = array_filter(mpull($page, 'getBuildTargetPHID')); 42 + if ($build_target_phids) { 43 + $build_targets = id(new PhabricatorObjectQuery()) 44 + ->setViewer($this->getViewer()) 45 + ->withPHIDs($build_target_phids) 46 + ->setParentQuery($this) 47 + ->execute(); 48 + $build_targets = mpull($build_targets, null, 'getPHID'); 49 + } else { 50 + $build_targets = array(); 51 + } 52 + 53 + foreach ($page as $key => $message) { 54 + $build_target_phid = $message->getBuildTargetPHID(); 55 + if (empty($build_targets[$build_target_phid])) { 56 + unset($page[$key]); 57 + continue; 58 + } 59 + $message->attachBuildTarget($build_targets[$build_target_phid]); 60 + } 61 + 62 + return $page; 63 + } 64 + 65 + private function buildWhereClause(AphrontDatabaseConnection $conn_r) { 66 + $where = array(); 67 + 68 + if ($this->ids) { 69 + $where[] = qsprintf( 70 + $conn_r, 71 + 'id IN (%Ld)', 72 + $this->ids); 73 + } 74 + 75 + if ($this->buildTargetPHIDs) { 76 + $where[] = qsprintf( 77 + $conn_r, 78 + 'buildTargetPHID IN (%Ls)', 79 + $this->buildTargetPHIDs); 80 + } 81 + 82 + if ($this->consumed !== null) { 83 + $where[] = qsprintf( 84 + $conn_r, 85 + 'isConsumed = %d', 86 + (int)$this->isConsumed); 87 + } 88 + 89 + $where[] = $this->buildPagingClause($conn_r); 90 + 91 + return $this->formatWhereClause($where); 92 + } 93 + 94 + public function getQueryApplicationClass() { 95 + return 'PhabricatorApplicationHarbormaster'; 96 + } 97 + 98 + }
+58
src/applications/harbormaster/storage/HarbormasterBuildMessage.php
··· 1 + <?php 2 + 3 + /** 4 + * A message sent to an executing build target by an external system. We 5 + * capture these messages and process them asynchronously to avoid race 6 + * conditions where we receive a message before a build plan is ready to 7 + * accept it. 8 + */ 9 + final class HarbormasterBuildMessage extends HarbormasterDAO 10 + implements PhabricatorPolicyInterface { 11 + 12 + protected $authorPHID; 13 + protected $buildTargetPHID; 14 + protected $type; 15 + protected $isConsumed; 16 + 17 + private $buildTarget = self::ATTACHABLE; 18 + 19 + public static function initializeNewMessage(PhabricatorUser $actor) { 20 + return id(new HarbormasterBuildMessage()) 21 + ->setAuthorPHID($actor->getPHID()) 22 + ->setIsConsumed(0); 23 + } 24 + 25 + public function getBuildTarget() { 26 + return $this->assertAttached($this->buildTarget); 27 + } 28 + 29 + public function attachBuildTarget(HarbormasterBuildTarget $target) { 30 + $this->buildTarget = $target; 31 + return $this; 32 + } 33 + 34 + 35 + /* -( PhabricatorPolicyInterface )----------------------------------------- */ 36 + 37 + 38 + public function getCapabilities() { 39 + return array( 40 + PhabricatorPolicyCapability::CAN_VIEW, 41 + ); 42 + } 43 + 44 + public function getPolicy($capability) { 45 + return $this->getBuildTarget()->getPolicy($capability); 46 + } 47 + 48 + public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { 49 + return $this->getBuildTarget()->hasAutomaticCapability( 50 + $capability, 51 + $viewer); 52 + } 53 + 54 + public function describeAutomaticCapability($capability) { 55 + return pht('Build messages have the same policies as their targets.'); 56 + } 57 + 58 + }
+5 -2
src/applications/harbormaster/storage/build/HarbormasterBuild.php
··· 168 168 'repository.vcs' => null, 169 169 'repository.uri' => null, 170 170 'step.timestamp' => null, 171 - 'build.id' => null); 171 + 'build.id' => null, 172 + ); 172 173 173 174 $buildable = $this->getBuildable(); 174 175 $object = $buildable->getBuildableObject(); ··· 210 211 'repository.uri' => 211 212 pht('The URI to clone or checkout the repository from.'), 212 213 'step.timestamp' => pht('The current UNIX timestamp.'), 213 - 'build.id' => pht('The ID of the current build.')); 214 + 'build.id' => pht('The ID of the current build.'), 215 + 'target.phid' => pht('The PHID of the current build target.'), 216 + ); 214 217 } 215 218 216 219 public function isComplete() {
+10
src/applications/harbormaster/storage/build/HarbormasterBuildTarget.php
··· 73 73 return $this; 74 74 } 75 75 76 + public function getVariables() { 77 + return parent::getVariables() + $this->getBuildTargetVariables(); 78 + } 79 + 76 80 public function getVariable($key, $default = null) { 77 81 return idx($this->variables, $key, $default); 78 82 } ··· 91 95 } 92 96 93 97 return $this->implementation; 98 + } 99 + 100 + private function getBuildTargetVariables() { 101 + return array( 102 + 'target.phid' => $this->getPHID(), 103 + ); 94 104 } 95 105 96 106