PageRenderTime 41ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/src/applications/harbormaster/storage/build/HarbormasterBuild.php

http://github.com/facebook/phabricator
PHP | 582 lines | 465 code | 101 blank | 16 comment | 19 complexity | c1a5bd233ed1abadd44c30540a71f579 MD5 | raw file
Possible License(s): JSON, MPL-2.0-no-copyleft-exception, Apache-2.0, BSD-3-Clause, LGPL-2.0, MIT, LGPL-2.1, LGPL-3.0
  1. <?php
  2. final class HarbormasterBuild extends HarbormasterDAO
  3. implements
  4. PhabricatorApplicationTransactionInterface,
  5. PhabricatorPolicyInterface,
  6. PhabricatorConduitResultInterface,
  7. PhabricatorDestructibleInterface {
  8. protected $buildablePHID;
  9. protected $buildPlanPHID;
  10. protected $buildStatus;
  11. protected $buildGeneration;
  12. protected $buildParameters = array();
  13. protected $initiatorPHID;
  14. protected $planAutoKey;
  15. private $buildable = self::ATTACHABLE;
  16. private $buildPlan = self::ATTACHABLE;
  17. private $buildTargets = self::ATTACHABLE;
  18. private $unprocessedCommands = self::ATTACHABLE;
  19. public static function initializeNewBuild(PhabricatorUser $actor) {
  20. return id(new HarbormasterBuild())
  21. ->setBuildStatus(HarbormasterBuildStatus::STATUS_INACTIVE)
  22. ->setBuildGeneration(0);
  23. }
  24. public function delete() {
  25. $this->openTransaction();
  26. $this->deleteUnprocessedCommands();
  27. $result = parent::delete();
  28. $this->saveTransaction();
  29. return $result;
  30. }
  31. protected function getConfiguration() {
  32. return array(
  33. self::CONFIG_AUX_PHID => true,
  34. self::CONFIG_SERIALIZATION => array(
  35. 'buildParameters' => self::SERIALIZATION_JSON,
  36. ),
  37. self::CONFIG_COLUMN_SCHEMA => array(
  38. 'buildStatus' => 'text32',
  39. 'buildGeneration' => 'uint32',
  40. 'planAutoKey' => 'text32?',
  41. 'initiatorPHID' => 'phid?',
  42. ),
  43. self::CONFIG_KEY_SCHEMA => array(
  44. 'key_buildable' => array(
  45. 'columns' => array('buildablePHID'),
  46. ),
  47. 'key_plan' => array(
  48. 'columns' => array('buildPlanPHID'),
  49. ),
  50. 'key_status' => array(
  51. 'columns' => array('buildStatus'),
  52. ),
  53. 'key_planautokey' => array(
  54. 'columns' => array('buildablePHID', 'planAutoKey'),
  55. 'unique' => true,
  56. ),
  57. 'key_initiator' => array(
  58. 'columns' => array('initiatorPHID'),
  59. ),
  60. ),
  61. ) + parent::getConfiguration();
  62. }
  63. public function generatePHID() {
  64. return PhabricatorPHID::generateNewPHID(
  65. HarbormasterBuildPHIDType::TYPECONST);
  66. }
  67. public function attachBuildable(HarbormasterBuildable $buildable) {
  68. $this->buildable = $buildable;
  69. return $this;
  70. }
  71. public function getBuildable() {
  72. return $this->assertAttached($this->buildable);
  73. }
  74. public function getName() {
  75. if ($this->getBuildPlan()) {
  76. return $this->getBuildPlan()->getName();
  77. }
  78. return pht('Build');
  79. }
  80. public function attachBuildPlan(
  81. HarbormasterBuildPlan $build_plan = null) {
  82. $this->buildPlan = $build_plan;
  83. return $this;
  84. }
  85. public function getBuildPlan() {
  86. return $this->assertAttached($this->buildPlan);
  87. }
  88. public function getBuildTargets() {
  89. return $this->assertAttached($this->buildTargets);
  90. }
  91. public function attachBuildTargets(array $targets) {
  92. $this->buildTargets = $targets;
  93. return $this;
  94. }
  95. public function isBuilding() {
  96. return $this->getBuildStatusObject()->isBuilding();
  97. }
  98. public function isAutobuild() {
  99. return ($this->getPlanAutoKey() !== null);
  100. }
  101. public function retrieveVariablesFromBuild() {
  102. $results = array(
  103. 'buildable.diff' => null,
  104. 'buildable.revision' => null,
  105. 'buildable.commit' => null,
  106. 'repository.callsign' => null,
  107. 'repository.phid' => null,
  108. 'repository.vcs' => null,
  109. 'repository.uri' => null,
  110. 'step.timestamp' => null,
  111. 'build.id' => null,
  112. 'initiator.phid' => null,
  113. );
  114. foreach ($this->getBuildParameters() as $key => $value) {
  115. $results['build/'.$key] = $value;
  116. }
  117. $buildable = $this->getBuildable();
  118. $object = $buildable->getBuildableObject();
  119. $object_variables = $object->getBuildVariables();
  120. $results = $object_variables + $results;
  121. $results['step.timestamp'] = time();
  122. $results['build.id'] = $this->getID();
  123. $results['initiator.phid'] = $this->getInitiatorPHID();
  124. return $results;
  125. }
  126. public static function getAvailableBuildVariables() {
  127. $objects = id(new PhutilClassMapQuery())
  128. ->setAncestorClass('HarbormasterBuildableInterface')
  129. ->execute();
  130. $variables = array();
  131. $variables[] = array(
  132. 'step.timestamp' => pht('The current UNIX timestamp.'),
  133. 'build.id' => pht('The ID of the current build.'),
  134. 'target.phid' => pht('The PHID of the current build target.'),
  135. 'initiator.phid' => pht(
  136. 'The PHID of the user or Object that initiated the build, '.
  137. 'if applicable.'),
  138. );
  139. foreach ($objects as $object) {
  140. $variables[] = $object->getAvailableBuildVariables();
  141. }
  142. $variables = array_mergev($variables);
  143. return $variables;
  144. }
  145. public function isComplete() {
  146. return $this->getBuildStatusObject()->isComplete();
  147. }
  148. public function isPaused() {
  149. return $this->getBuildStatusObject()->isPaused();
  150. }
  151. public function isPassed() {
  152. return $this->getBuildStatusObject()->isPassed();
  153. }
  154. public function isFailed() {
  155. return $this->getBuildStatusObject()->isFailed();
  156. }
  157. public function getURI() {
  158. $id = $this->getID();
  159. return "/harbormaster/build/{$id}/";
  160. }
  161. protected function getBuildStatusObject() {
  162. $status_key = $this->getBuildStatus();
  163. return HarbormasterBuildStatus::newBuildStatusObject($status_key);
  164. }
  165. public function getObjectName() {
  166. return pht('Build %d', $this->getID());
  167. }
  168. /* -( Build Commands )----------------------------------------------------- */
  169. private function getUnprocessedCommands() {
  170. return $this->assertAttached($this->unprocessedCommands);
  171. }
  172. public function attachUnprocessedCommands(array $commands) {
  173. $this->unprocessedCommands = $commands;
  174. return $this;
  175. }
  176. public function canRestartBuild() {
  177. try {
  178. $this->assertCanRestartBuild();
  179. return true;
  180. } catch (HarbormasterRestartException $ex) {
  181. return false;
  182. }
  183. }
  184. public function assertCanRestartBuild() {
  185. if ($this->isAutobuild()) {
  186. throw new HarbormasterRestartException(
  187. pht('Can Not Restart Autobuild'),
  188. pht(
  189. 'This build can not be restarted because it is an automatic '.
  190. 'build.'));
  191. }
  192. $restartable = HarbormasterBuildPlanBehavior::BEHAVIOR_RESTARTABLE;
  193. $plan = $this->getBuildPlan();
  194. // See T13526. Users who can't see the "BuildPlan" can end up here with
  195. // no object. This is highly questionable.
  196. if (!$plan) {
  197. throw new HarbormasterRestartException(
  198. pht('No Build Plan Permission'),
  199. pht(
  200. 'You can not restart this build because you do not have '.
  201. 'permission to access the build plan.'));
  202. }
  203. $option = HarbormasterBuildPlanBehavior::getBehavior($restartable)
  204. ->getPlanOption($plan);
  205. $option_key = $option->getKey();
  206. $never_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_NEVER;
  207. $is_never = ($option_key === $never_restartable);
  208. if ($is_never) {
  209. throw new HarbormasterRestartException(
  210. pht('Build Plan Prevents Restart'),
  211. pht(
  212. 'This build can not be restarted because the build plan is '.
  213. 'configured to prevent the build from restarting.'));
  214. }
  215. $failed_restartable = HarbormasterBuildPlanBehavior::RESTARTABLE_IF_FAILED;
  216. $is_failed = ($option_key === $failed_restartable);
  217. if ($is_failed) {
  218. if (!$this->isFailed()) {
  219. throw new HarbormasterRestartException(
  220. pht('Only Restartable if Failed'),
  221. pht(
  222. 'This build can not be restarted because the build plan is '.
  223. 'configured to prevent the build from restarting unless it '.
  224. 'has failed, and it has not failed.'));
  225. }
  226. }
  227. if ($this->isRestarting()) {
  228. throw new HarbormasterRestartException(
  229. pht('Already Restarting'),
  230. pht(
  231. 'This build is already restarting. You can not reissue a restart '.
  232. 'command to a restarting build.'));
  233. }
  234. }
  235. public function canPauseBuild() {
  236. if ($this->isAutobuild()) {
  237. return false;
  238. }
  239. return !$this->isComplete() &&
  240. !$this->isPaused() &&
  241. !$this->isPausing();
  242. }
  243. public function canAbortBuild() {
  244. if ($this->isAutobuild()) {
  245. return false;
  246. }
  247. return !$this->isComplete();
  248. }
  249. public function canResumeBuild() {
  250. if ($this->isAutobuild()) {
  251. return false;
  252. }
  253. return $this->isPaused() &&
  254. !$this->isResuming();
  255. }
  256. public function isPausing() {
  257. $is_pausing = false;
  258. foreach ($this->getUnprocessedCommands() as $command_object) {
  259. $command = $command_object->getCommand();
  260. switch ($command) {
  261. case HarbormasterBuildCommand::COMMAND_PAUSE:
  262. $is_pausing = true;
  263. break;
  264. case HarbormasterBuildCommand::COMMAND_RESUME:
  265. case HarbormasterBuildCommand::COMMAND_RESTART:
  266. $is_pausing = false;
  267. break;
  268. case HarbormasterBuildCommand::COMMAND_ABORT:
  269. $is_pausing = true;
  270. break;
  271. }
  272. }
  273. return $is_pausing;
  274. }
  275. public function isResuming() {
  276. $is_resuming = false;
  277. foreach ($this->getUnprocessedCommands() as $command_object) {
  278. $command = $command_object->getCommand();
  279. switch ($command) {
  280. case HarbormasterBuildCommand::COMMAND_RESTART:
  281. case HarbormasterBuildCommand::COMMAND_RESUME:
  282. $is_resuming = true;
  283. break;
  284. case HarbormasterBuildCommand::COMMAND_PAUSE:
  285. $is_resuming = false;
  286. break;
  287. case HarbormasterBuildCommand::COMMAND_ABORT:
  288. $is_resuming = false;
  289. break;
  290. }
  291. }
  292. return $is_resuming;
  293. }
  294. public function isRestarting() {
  295. $is_restarting = false;
  296. foreach ($this->getUnprocessedCommands() as $command_object) {
  297. $command = $command_object->getCommand();
  298. switch ($command) {
  299. case HarbormasterBuildCommand::COMMAND_RESTART:
  300. $is_restarting = true;
  301. break;
  302. }
  303. }
  304. return $is_restarting;
  305. }
  306. public function isAborting() {
  307. $is_aborting = false;
  308. foreach ($this->getUnprocessedCommands() as $command_object) {
  309. $command = $command_object->getCommand();
  310. switch ($command) {
  311. case HarbormasterBuildCommand::COMMAND_ABORT:
  312. $is_aborting = true;
  313. break;
  314. }
  315. }
  316. return $is_aborting;
  317. }
  318. public function deleteUnprocessedCommands() {
  319. foreach ($this->getUnprocessedCommands() as $key => $command_object) {
  320. $command_object->delete();
  321. unset($this->unprocessedCommands[$key]);
  322. }
  323. return $this;
  324. }
  325. public function canIssueCommand(PhabricatorUser $viewer, $command) {
  326. try {
  327. $this->assertCanIssueCommand($viewer, $command);
  328. return true;
  329. } catch (Exception $ex) {
  330. return false;
  331. }
  332. }
  333. public function assertCanIssueCommand(PhabricatorUser $viewer, $command) {
  334. $plan = $this->getBuildPlan();
  335. // See T13526. Users without permission to access the build plan can
  336. // currently end up here with no "BuildPlan" object.
  337. if (!$plan) {
  338. return false;
  339. }
  340. $need_edit = true;
  341. switch ($command) {
  342. case HarbormasterBuildCommand::COMMAND_RESTART:
  343. case HarbormasterBuildCommand::COMMAND_PAUSE:
  344. case HarbormasterBuildCommand::COMMAND_RESUME:
  345. case HarbormasterBuildCommand::COMMAND_ABORT:
  346. if ($plan->canRunWithoutEditCapability()) {
  347. $need_edit = false;
  348. }
  349. break;
  350. default:
  351. throw new Exception(
  352. pht(
  353. 'Invalid Harbormaster build command "%s".',
  354. $command));
  355. }
  356. // Issuing these commands requires that you be able to edit the build, to
  357. // prevent enemy engineers from sabotaging your builds. See T9614.
  358. if ($need_edit) {
  359. PhabricatorPolicyFilter::requireCapability(
  360. $viewer,
  361. $plan,
  362. PhabricatorPolicyCapability::CAN_EDIT);
  363. }
  364. }
  365. public function sendMessage(PhabricatorUser $viewer, $command) {
  366. // TODO: This should not be an editor transaction, but there are plans to
  367. // merge BuildCommand into BuildMessage which should moot this. As this
  368. // exists today, it can race against BuildEngine.
  369. // This is a bogus content source, but this whole flow should be obsolete
  370. // soon.
  371. $content_source = PhabricatorContentSource::newForSource(
  372. PhabricatorConsoleContentSource::SOURCECONST);
  373. $editor = id(new HarbormasterBuildTransactionEditor())
  374. ->setActor($viewer)
  375. ->setContentSource($content_source)
  376. ->setContinueOnNoEffect(true)
  377. ->setContinueOnMissingFields(true);
  378. $viewer_phid = $viewer->getPHID();
  379. if (!$viewer_phid) {
  380. $acting_phid = id(new PhabricatorHarbormasterApplication())->getPHID();
  381. $editor->setActingAsPHID($acting_phid);
  382. }
  383. $xaction = id(new HarbormasterBuildTransaction())
  384. ->setTransactionType(HarbormasterBuildTransaction::TYPE_COMMAND)
  385. ->setNewValue($command);
  386. $editor->applyTransactions($this, array($xaction));
  387. }
  388. /* -( PhabricatorApplicationTransactionInterface )------------------------- */
  389. public function getApplicationTransactionEditor() {
  390. return new HarbormasterBuildTransactionEditor();
  391. }
  392. public function getApplicationTransactionTemplate() {
  393. return new HarbormasterBuildTransaction();
  394. }
  395. /* -( PhabricatorPolicyInterface )----------------------------------------- */
  396. public function getCapabilities() {
  397. return array(
  398. PhabricatorPolicyCapability::CAN_VIEW,
  399. PhabricatorPolicyCapability::CAN_EDIT,
  400. );
  401. }
  402. public function getPolicy($capability) {
  403. return $this->getBuildable()->getPolicy($capability);
  404. }
  405. public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
  406. return $this->getBuildable()->hasAutomaticCapability(
  407. $capability,
  408. $viewer);
  409. }
  410. public function describeAutomaticCapability($capability) {
  411. return pht('A build inherits policies from its buildable.');
  412. }
  413. /* -( PhabricatorConduitResultInterface )---------------------------------- */
  414. public function getFieldSpecificationsForConduit() {
  415. return array(
  416. id(new PhabricatorConduitSearchFieldSpecification())
  417. ->setKey('buildablePHID')
  418. ->setType('phid')
  419. ->setDescription(pht('PHID of the object this build is building.')),
  420. id(new PhabricatorConduitSearchFieldSpecification())
  421. ->setKey('buildPlanPHID')
  422. ->setType('phid')
  423. ->setDescription(pht('PHID of the build plan being run.')),
  424. id(new PhabricatorConduitSearchFieldSpecification())
  425. ->setKey('buildStatus')
  426. ->setType('map<string, wild>')
  427. ->setDescription(pht('The current status of this build.')),
  428. id(new PhabricatorConduitSearchFieldSpecification())
  429. ->setKey('initiatorPHID')
  430. ->setType('phid')
  431. ->setDescription(pht('The person (or thing) that started this build.')),
  432. id(new PhabricatorConduitSearchFieldSpecification())
  433. ->setKey('name')
  434. ->setType('string')
  435. ->setDescription(pht('The name of this build.')),
  436. );
  437. }
  438. public function getFieldValuesForConduit() {
  439. $status = $this->getBuildStatus();
  440. return array(
  441. 'buildablePHID' => $this->getBuildablePHID(),
  442. 'buildPlanPHID' => $this->getBuildPlanPHID(),
  443. 'buildStatus' => array(
  444. 'value' => $status,
  445. 'name' => HarbormasterBuildStatus::getBuildStatusName($status),
  446. 'color.ansi' =>
  447. HarbormasterBuildStatus::getBuildStatusANSIColor($status),
  448. ),
  449. 'initiatorPHID' => nonempty($this->getInitiatorPHID(), null),
  450. 'name' => $this->getName(),
  451. );
  452. }
  453. public function getConduitSearchAttachments() {
  454. return array(
  455. id(new HarbormasterQueryBuildsSearchEngineAttachment())
  456. ->setAttachmentKey('querybuilds'),
  457. );
  458. }
  459. /* -( PhabricatorDestructibleInterface )----------------------------------- */
  460. public function destroyObjectPermanently(
  461. PhabricatorDestructionEngine $engine) {
  462. $viewer = $engine->getViewer();
  463. $this->openTransaction();
  464. $targets = id(new HarbormasterBuildTargetQuery())
  465. ->setViewer($viewer)
  466. ->withBuildPHIDs(array($this->getPHID()))
  467. ->execute();
  468. foreach ($targets as $target) {
  469. $engine->destroyObject($target);
  470. }
  471. $messages = id(new HarbormasterBuildMessageQuery())
  472. ->setViewer($viewer)
  473. ->withReceiverPHIDs(array($this->getPHID()))
  474. ->execute();
  475. foreach ($messages as $message) {
  476. $engine->destroyObject($message);
  477. }
  478. $this->delete();
  479. $this->saveTransaction();
  480. }
  481. }