3 final class DrydockWorkingCopyBlueprintImplementation
4 extends DrydockBlueprintImplementation
{
6 const PHASE_SQUASHMERGE
= 'squashmerge';
7 const PHASE_REMOTEFETCH
= 'blueprint.workingcopy.fetch.remote';
8 const PHASE_MERGEFETCH
= 'blueprint.workingcopy.fetch.staging';
10 public function isEnabled() {
14 public function getBlueprintName() {
15 return pht('Working Copy');
18 public function getBlueprintIcon() {
19 return 'fa-folder-open';
22 public function getDescription() {
23 return pht('Allows Drydock to check out working copies of repositories.');
26 public function canAnyBlueprintEverAllocateResourceForLease(
27 DrydockLease
$lease) {
31 public function canEverAllocateResourceForLease(
32 DrydockBlueprint
$blueprint,
33 DrydockLease
$lease) {
37 public function canAllocateResourceForLease(
38 DrydockBlueprint
$blueprint,
39 DrydockLease
$lease) {
40 $viewer = $this->getViewer();
42 if ($this->shouldLimitAllocatingPoolSize($blueprint)) {
46 // TODO: If we have a pending resource which is compatible with the
47 // configuration for this lease, prevent a new allocation? Otherwise the
48 // queue can fill up with copies of requests from the same lease. But
49 // maybe we can deal with this with "pre-leasing"?
54 public function canAcquireLeaseOnResource(
55 DrydockBlueprint
$blueprint,
56 DrydockResource
$resource,
57 DrydockLease
$lease) {
59 // Don't hand out leases on working copies which have not activated, since
60 // it may take an arbitrarily long time for them to acquire a host.
61 if (!$resource->isActive()) {
65 $need_map = $lease->getAttribute('repositories.map');
66 if (!is_array($need_map)) {
70 $have_map = $resource->getAttribute('repositories.map');
71 if (!is_array($have_map)) {
75 $have_as = ipull($have_map, 'phid');
76 $need_as = ipull($need_map, 'phid');
78 foreach ($need_as as $need_directory => $need_phid) {
79 if (empty($have_as[$need_directory])) {
80 // This resource is missing a required working copy.
84 if ($have_as[$need_directory] != $need_phid) {
85 // This resource has a required working copy, but it contains
86 // the wrong repository.
90 unset($have_as[$need_directory]);
93 if ($have_as && $lease->getAttribute('repositories.strict')) {
94 // This resource has extra repositories, but the lease is strict about
95 // which repositories are allowed to exist.
99 if (!DrydockSlotLock
::isLockFree($this->getLeaseSlotLock($resource))) {
106 public function acquireLease(
107 DrydockBlueprint
$blueprint,
108 DrydockResource
$resource,
109 DrydockLease
$lease) {
112 ->needSlotLock($this->getLeaseSlotLock($resource))
113 ->acquireOnResource($resource);
116 private function getLeaseSlotLock(DrydockResource
$resource) {
117 $resource_phid = $resource->getPHID();
118 return "workingcopy.lease({$resource_phid})";
121 public function allocateResource(
122 DrydockBlueprint
$blueprint,
123 DrydockLease
$lease) {
125 $resource = $this->newResourceTemplate($blueprint);
127 $resource_phid = $resource->getPHID();
129 $blueprint_phids = $blueprint->getFieldValue('blueprintPHIDs');
131 $host_lease = $this->newLease($blueprint)
132 ->setResourceType('host')
133 ->setOwnerPHID($resource_phid)
134 ->setAttribute('workingcopy.resourcePHID', $resource_phid)
135 ->setAllowedBlueprintPHIDs($blueprint_phids);
136 $resource->setAttribute('host.leasePHID', $host_lease->getPHID());
138 $map = $lease->getAttribute('repositories.map');
139 foreach ($map as $key => $value) {
140 $map[$key] = array_select_keys(
146 $resource->setAttribute('repositories.map', $map);
148 $slot_lock = $this->getConcurrentResourceLimitSlotLock($blueprint);
149 if ($slot_lock !== null) {
150 $resource->needSlotLock($slot_lock);
153 $resource->allocateResource();
155 $host_lease->queueForActivation();
160 public function activateResource(
161 DrydockBlueprint
$blueprint,
162 DrydockResource
$resource) {
164 $lease = $this->loadHostLease($resource);
165 $this->requireActiveLease($lease);
167 $command_type = DrydockCommandInterface
::INTERFACE_TYPE
;
168 $interface = $lease->getInterface($command_type);
170 // TODO: Make this configurable.
171 $resource_id = $resource->getID();
172 $root = "/var/drydock/workingcopy-{$resource_id}";
174 $map = $resource->getAttribute('repositories.map');
177 $repositories = $this->loadRepositories(ipull($map, 'phid'));
178 foreach ($map as $directory => $spec) {
179 // TODO: Validate directory isn't goofy like "/etc" or "../../lol"
182 $repository = $repositories[$spec['phid']];
183 $path = "{$root}/repo/{$directory}/";
185 $future = $interface->getExecFuture(
186 'git clone -- %s %s',
187 (string)$repository->getCloneURIObject(),
190 $future->setTimeout($repository->getEffectiveCopyTimeLimit());
192 $futures[$directory] = $future;
195 foreach (new FutureIterator($futures) as $key => $future) {
200 ->setAttribute('workingcopy.root', $root)
201 ->activateResource();
204 public function destroyResource(
205 DrydockBlueprint
$blueprint,
206 DrydockResource
$resource) {
209 $lease = $this->loadHostLease($resource);
210 } catch (Exception
$ex) {
211 // If we can't load the lease, assume we don't need to take any actions
216 // Destroy the lease on the host.
217 $lease->setReleaseOnDestruction(true);
219 if ($lease->isActive()) {
220 // Destroy the working copy on disk.
221 $command_type = DrydockCommandInterface
::INTERFACE_TYPE
;
222 $interface = $lease->getInterface($command_type);
224 $root_key = 'workingcopy.root';
225 $root = $resource->getAttribute($root_key);
227 $interface->execx('rm -rf -- %s', $root);
232 public function getResourceName(
233 DrydockBlueprint
$blueprint,
234 DrydockResource
$resource) {
235 return pht('Working Copy');
239 public function activateLease(
240 DrydockBlueprint
$blueprint,
241 DrydockResource
$resource,
242 DrydockLease
$lease) {
244 $host_lease = $this->loadHostLease($resource);
245 $command_type = DrydockCommandInterface
::INTERFACE_TYPE
;
246 $interface = $host_lease->getInterface($command_type);
248 $map = $lease->getAttribute('repositories.map');
249 $root = $resource->getAttribute('workingcopy.root');
251 $repositories = $this->loadRepositories(ipull($map, 'phid'));
254 foreach ($map as $directory => $spec) {
255 $repository = $repositories[$spec['phid']];
257 $interface->pushWorkingDirectory("{$root}/repo/{$directory}/");
262 $cmd[] = 'git clean -d --force';
263 $cmd[] = 'git fetch';
265 $commit = idx($spec, 'commit');
266 $branch = idx($spec, 'branch');
268 $ref = idx($spec, 'ref');
270 // Reset things first, in case previous builds left anything staged or
271 // dirty. Note that we don't reset to "HEAD" because that does not work
272 // in empty repositories.
273 $cmd[] = 'git reset --hard';
275 if ($commit !== null) {
276 $cmd[] = 'git checkout %s --';
278 } else if ($branch !== null) {
279 $cmd[] = 'git checkout %s --';
282 $cmd[] = 'git reset --hard origin/%s';
286 $this->newExecvFuture($interface, $cmd, $arg)
287 ->setTimeout($repository->getEffectiveCopyTimeLimit())
290 if (idx($spec, 'default')) {
291 $default = $directory;
294 // If we're fetching a ref from a remote, do that separately so we can
295 // raise a more tailored error.
300 $ref_uri = $ref['uri'];
301 $ref_ref = $ref['ref'];
303 $cmd[] = 'git fetch --no-tags -- %s +%s:%s';
308 $cmd[] = 'git checkout %s --';
312 $this->newExecvFuture($interface, $cmd, $arg)
313 ->setTimeout($repository->getEffectiveCopyTimeLimit())
315 } catch (CommandException
$ex) {
316 $display_command = csprintf(
321 $error = DrydockCommandError
::newFromCommandException($ex)
322 ->setPhase(self
::PHASE_REMOTEFETCH
)
323 ->setDisplayCommand($display_command);
325 $lease->setAttribute(
326 'workingcopy.vcs.error',
327 $error->toDictionary());
333 $merges = idx($spec, 'merges');
335 foreach ($merges as $merge) {
336 $this->applyMerge($lease, $interface, $merge);
340 $interface->popWorkingDirectory();
343 if ($default === null) {
344 $default = head_key($map);
347 // TODO: Use working storage?
348 $lease->setAttribute('workingcopy.default', "{$root}/repo/{$default}/");
350 $lease->activateOnResource($resource);
353 public function didReleaseLease(
354 DrydockBlueprint
$blueprint,
355 DrydockResource
$resource,
356 DrydockLease
$lease) {
357 // We leave working copies around even if there are no leases on them,
358 // since the cost to maintain them is nearly zero but rebuilding them is
359 // moderately expensive and it's likely that they'll be reused.
363 public function destroyLease(
364 DrydockBlueprint
$blueprint,
365 DrydockResource
$resource,
366 DrydockLease
$lease) {
367 // When we activate a lease we just reset the working copy state and do
368 // not create any new state, so we don't need to do anything special when
369 // destroying a lease.
373 public function getType() {
374 return 'working-copy';
377 public function getInterface(
378 DrydockBlueprint
$blueprint,
379 DrydockResource
$resource,
384 case DrydockCommandInterface
::INTERFACE_TYPE
:
385 $host_lease = $this->loadHostLease($resource);
386 $command_interface = $host_lease->getInterface($type);
388 $path = $lease->getAttribute('workingcopy.default');
389 $command_interface->pushWorkingDirectory($path);
391 return $command_interface;
395 private function loadRepositories(array $phids) {
396 $viewer = $this->getViewer();
398 $repositories = id(new PhabricatorRepositoryQuery())
402 $repositories = mpull($repositories, null, 'getPHID');
404 foreach ($phids as $phid) {
405 if (empty($repositories[$phid])) {
408 'Repository PHID "%s" does not exist.',
413 foreach ($repositories as $repository) {
414 $repository_vcs = $repository->getVersionControlSystem();
415 switch ($repository_vcs) {
416 case PhabricatorRepositoryType
::REPOSITORY_TYPE_GIT
:
421 'Repository ("%s") has unsupported VCS ("%s").',
422 $repository->getPHID(),
427 return $repositories;
430 private function loadHostLease(DrydockResource
$resource) {
431 $viewer = $this->getViewer();
433 $lease_phid = $resource->getAttribute('host.leasePHID');
435 $lease = id(new DrydockLeaseQuery())
437 ->withPHIDs(array($lease_phid))
442 'Unable to load lease ("%s").',
449 protected function getCustomFieldSpecifications() {
451 'blueprintPHIDs' => array(
452 'name' => pht('Use Blueprints'),
453 'type' => 'blueprints',
459 protected function shouldUseConcurrentResourceLimit() {
463 private function applyMerge(
465 DrydockCommandInterface
$interface,
468 $src_uri = $merge['src.uri'];
469 $src_ref = $merge['src.ref'];
474 'git fetch --no-tags -- %s +%s:%s',
478 } catch (CommandException
$ex) {
479 $display_command = csprintf(
480 'git fetch %R +%R:%R',
485 $error = DrydockCommandError
::newFromCommandException($ex)
486 ->setPhase(self
::PHASE_MERGEFETCH
)
487 ->setDisplayCommand($display_command);
489 $lease->setAttribute('workingcopy.vcs.error', $error->toDictionary());
495 // NOTE: This can never actually generate a commit because we pass
496 // "--squash", but git sometimes runs code to check that a username and
497 // email are configured anyway.
498 $real_command = csprintf(
499 'git -c user.name=%s -c user.email=%s merge --no-stat --squash -- %R',
501 'drydock@phabricator',
505 $interface->execx('%C', $real_command);
506 } catch (CommandException
$ex) {
507 $display_command = csprintf(
508 'git merge --squash %R',
511 $error = DrydockCommandError
::newFromCommandException($ex)
512 ->setPhase(self
::PHASE_SQUASHMERGE
)
513 ->setDisplayCommand($display_command);
515 $lease->setAttribute('workingcopy.vcs.error', $error->toDictionary());
520 public function getCommandError(DrydockLease
$lease) {
521 return $lease->getAttribute('workingcopy.vcs.error');
524 private function execxv(
525 DrydockCommandInterface
$interface,
528 return $this->newExecvFuture($interface, $commands, $arguments)->resolvex();
531 private function newExecvFuture(
532 DrydockCommandInterface
$interface,
536 $commands = implode(' && ', $commands);
537 $argv = array_merge(array($commands), $arguments);
539 return call_user_func_array(array($interface, 'getExecFuture'), $argv);