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)) {
49 public function canAcquireLeaseOnResource(
50 DrydockBlueprint
$blueprint,
51 DrydockResource
$resource,
52 DrydockLease
$lease) {
54 // Don't hand out leases on working copies which have not activated, since
55 // it may take an arbitrarily long time for them to acquire a host.
56 if (!$resource->isActive()) {
60 $need_map = $lease->getAttribute('repositories.map');
61 if (!is_array($need_map)) {
65 $have_map = $resource->getAttribute('repositories.map');
66 if (!is_array($have_map)) {
70 $have_as = ipull($have_map, 'phid');
71 $need_as = ipull($need_map, 'phid');
73 foreach ($need_as as $need_directory => $need_phid) {
74 if (empty($have_as[$need_directory])) {
75 // This resource is missing a required working copy.
79 if ($have_as[$need_directory] != $need_phid) {
80 // This resource has a required working copy, but it contains
81 // the wrong repository.
85 unset($have_as[$need_directory]);
88 if ($have_as && $lease->getAttribute('repositories.strict')) {
89 // This resource has extra repositories, but the lease is strict about
90 // which repositories are allowed to exist.
94 if (!DrydockSlotLock
::isLockFree($this->getLeaseSlotLock($resource))) {
101 public function acquireLease(
102 DrydockBlueprint
$blueprint,
103 DrydockResource
$resource,
104 DrydockLease
$lease) {
107 ->needSlotLock($this->getLeaseSlotLock($resource))
108 ->acquireOnResource($resource);
111 private function getLeaseSlotLock(DrydockResource
$resource) {
112 $resource_phid = $resource->getPHID();
113 return "workingcopy.lease({$resource_phid})";
116 public function allocateResource(
117 DrydockBlueprint
$blueprint,
118 DrydockLease
$lease) {
120 $resource = $this->newResourceTemplate($blueprint);
122 $resource_phid = $resource->getPHID();
124 $blueprint_phids = $blueprint->getFieldValue('blueprintPHIDs');
126 $host_lease = $this->newLease($blueprint)
127 ->setResourceType('host')
128 ->setOwnerPHID($resource_phid)
129 ->setAttribute('workingcopy.resourcePHID', $resource_phid)
130 ->setAllowedBlueprintPHIDs($blueprint_phids);
131 $resource->setAttribute('host.leasePHID', $host_lease->getPHID());
133 $map = $this->getWorkingCopyRepositoryMap($lease);
134 $resource->setAttribute('repositories.map', $map);
136 $slot_lock = $this->getConcurrentResourceLimitSlotLock($blueprint);
137 if ($slot_lock !== null) {
138 $resource->needSlotLock($slot_lock);
141 $resource->allocateResource();
143 $host_lease->queueForActivation();
148 private function getWorkingCopyRepositoryMap(DrydockLease
$lease) {
149 $attribute = 'repositories.map';
150 $map = $lease->getAttribute($attribute);
152 // TODO: Leases should validate their attributes more formally.
154 if (!is_array($map) ||
!$map) {
158 'Working copy lease is missing required attribute "%s".',
162 'Working copy lease has invalid attribute "%s".',
167 'Attribute "repositories.map" should be a map of repository '.
170 $message = implode("\n\n", $message);
172 throw new Exception($message);
175 foreach ($map as $key => $value) {
176 $map[$key] = array_select_keys(
186 public function activateResource(
187 DrydockBlueprint
$blueprint,
188 DrydockResource
$resource) {
190 $lease = $this->loadHostLease($resource);
191 $this->requireActiveLease($lease);
193 $command_type = DrydockCommandInterface
::INTERFACE_TYPE
;
194 $interface = $lease->getInterface($command_type);
196 // TODO: Make this configurable.
197 $resource_id = $resource->getID();
198 $root = "/var/drydock/workingcopy-{$resource_id}";
200 $map = $resource->getAttribute('repositories.map');
203 $repositories = $this->loadRepositories(ipull($map, 'phid'));
204 foreach ($map as $directory => $spec) {
205 // TODO: Validate directory isn't goofy like "/etc" or "../../lol"
208 $repository = $repositories[$spec['phid']];
209 $path = "{$root}/repo/{$directory}/";
211 $future = $interface->getExecFuture(
212 'git clone -- %s %s',
213 (string)$repository->getCloneURIObject(),
216 $future->setTimeout($repository->getEffectiveCopyTimeLimit());
218 $futures[$directory] = $future;
221 foreach (new FutureIterator($futures) as $key => $future) {
226 ->setAttribute('workingcopy.root', $root)
227 ->activateResource();
230 public function destroyResource(
231 DrydockBlueprint
$blueprint,
232 DrydockResource
$resource) {
235 $lease = $this->loadHostLease($resource);
236 } catch (Exception
$ex) {
237 // If we can't load the lease, assume we don't need to take any actions
242 // Destroy the lease on the host.
243 $lease->setReleaseOnDestruction(true);
245 if ($lease->isActive()) {
246 // Destroy the working copy on disk.
247 $command_type = DrydockCommandInterface
::INTERFACE_TYPE
;
248 $interface = $lease->getInterface($command_type);
250 $root_key = 'workingcopy.root';
251 $root = $resource->getAttribute($root_key);
253 $interface->execx('rm -rf -- %s', $root);
258 public function getResourceName(
259 DrydockBlueprint
$blueprint,
260 DrydockResource
$resource) {
261 return pht('Working Copy');
265 public function activateLease(
266 DrydockBlueprint
$blueprint,
267 DrydockResource
$resource,
268 DrydockLease
$lease) {
270 $host_lease = $this->loadHostLease($resource);
271 $command_type = DrydockCommandInterface
::INTERFACE_TYPE
;
272 $interface = $host_lease->getInterface($command_type);
274 $map = $lease->getAttribute('repositories.map');
275 $root = $resource->getAttribute('workingcopy.root');
277 $repositories = $this->loadRepositories(ipull($map, 'phid'));
280 foreach ($map as $directory => $spec) {
281 $repository = $repositories[$spec['phid']];
283 $interface->pushWorkingDirectory("{$root}/repo/{$directory}/");
288 $cmd[] = 'git clean -d --force';
289 $cmd[] = 'git fetch';
291 $commit = idx($spec, 'commit');
292 $branch = idx($spec, 'branch');
294 $ref = idx($spec, 'ref');
296 // Reset things first, in case previous builds left anything staged or
297 // dirty. Note that we don't reset to "HEAD" because that does not work
298 // in empty repositories.
299 $cmd[] = 'git reset --hard';
301 if ($commit !== null) {
302 $cmd[] = 'git checkout %s --';
304 } else if ($branch !== null) {
305 $cmd[] = 'git checkout %s --';
308 $cmd[] = 'git reset --hard origin/%s';
312 $this->newExecvFuture($interface, $cmd, $arg)
313 ->setTimeout($repository->getEffectiveCopyTimeLimit())
316 if (idx($spec, 'default')) {
317 $default = $directory;
320 // If we're fetching a ref from a remote, do that separately so we can
321 // raise a more tailored error.
326 $ref_uri = $ref['uri'];
327 $ref_ref = $ref['ref'];
329 $cmd[] = 'git fetch --no-tags -- %s +%s:%s';
334 $cmd[] = 'git checkout %s --';
338 $this->newExecvFuture($interface, $cmd, $arg)
339 ->setTimeout($repository->getEffectiveCopyTimeLimit())
341 } catch (CommandException
$ex) {
342 $display_command = csprintf(
347 $error = DrydockCommandError
::newFromCommandException($ex)
348 ->setPhase(self
::PHASE_REMOTEFETCH
)
349 ->setDisplayCommand($display_command);
351 $lease->setAttribute(
352 'workingcopy.vcs.error',
353 $error->toDictionary());
359 $merges = idx($spec, 'merges');
361 foreach ($merges as $merge) {
362 $this->applyMerge($lease, $interface, $merge);
366 $interface->popWorkingDirectory();
369 if ($default === null) {
370 $default = head_key($map);
373 // TODO: Use working storage?
374 $lease->setAttribute('workingcopy.default', "{$root}/repo/{$default}/");
376 $lease->activateOnResource($resource);
379 public function didReleaseLease(
380 DrydockBlueprint
$blueprint,
381 DrydockResource
$resource,
382 DrydockLease
$lease) {
383 // We leave working copies around even if there are no leases on them,
384 // since the cost to maintain them is nearly zero but rebuilding them is
385 // moderately expensive and it's likely that they'll be reused.
389 public function destroyLease(
390 DrydockBlueprint
$blueprint,
391 DrydockResource
$resource,
392 DrydockLease
$lease) {
393 // When we activate a lease we just reset the working copy state and do
394 // not create any new state, so we don't need to do anything special when
395 // destroying a lease.
399 public function getType() {
400 return 'working-copy';
403 public function getInterface(
404 DrydockBlueprint
$blueprint,
405 DrydockResource
$resource,
410 case DrydockCommandInterface
::INTERFACE_TYPE
:
411 $host_lease = $this->loadHostLease($resource);
412 $command_interface = $host_lease->getInterface($type);
414 $path = $lease->getAttribute('workingcopy.default');
415 $command_interface->pushWorkingDirectory($path);
417 return $command_interface;
421 private function loadRepositories(array $phids) {
422 $viewer = $this->getViewer();
424 $repositories = id(new PhabricatorRepositoryQuery())
428 $repositories = mpull($repositories, null, 'getPHID');
430 foreach ($phids as $phid) {
431 if (empty($repositories[$phid])) {
434 'Repository PHID "%s" does not exist.',
439 foreach ($repositories as $repository) {
440 $repository_vcs = $repository->getVersionControlSystem();
441 switch ($repository_vcs) {
442 case PhabricatorRepositoryType
::REPOSITORY_TYPE_GIT
:
447 'Repository ("%s") has unsupported VCS ("%s").',
448 $repository->getPHID(),
453 return $repositories;
456 private function loadHostLease(DrydockResource
$resource) {
457 $viewer = $this->getViewer();
459 $lease_phid = $resource->getAttribute('host.leasePHID');
461 $lease = id(new DrydockLeaseQuery())
463 ->withPHIDs(array($lease_phid))
468 'Unable to load lease ("%s").',
475 protected function getCustomFieldSpecifications() {
477 'blueprintPHIDs' => array(
478 'name' => pht('Use Blueprints'),
479 'type' => 'blueprints',
485 protected function shouldUseConcurrentResourceLimit() {
489 private function applyMerge(
491 DrydockCommandInterface
$interface,
494 $src_uri = $merge['src.uri'];
495 $src_ref = $merge['src.ref'];
500 'git fetch --no-tags -- %s +%s:%s',
504 } catch (CommandException
$ex) {
505 $display_command = csprintf(
506 'git fetch %R +%R:%R',
511 $error = DrydockCommandError
::newFromCommandException($ex)
512 ->setPhase(self
::PHASE_MERGEFETCH
)
513 ->setDisplayCommand($display_command);
515 $lease->setAttribute('workingcopy.vcs.error', $error->toDictionary());
521 // NOTE: This can never actually generate a commit because we pass
522 // "--squash", but git sometimes runs code to check that a username and
523 // email are configured anyway.
524 $real_command = csprintf(
525 'git -c user.name=%s -c user.email=%s merge --no-stat --squash -- %R',
527 'drydock@phabricator',
531 $interface->execx('%C', $real_command);
532 } catch (CommandException
$ex) {
533 $display_command = csprintf(
534 'git merge --squash %R',
537 $error = DrydockCommandError
::newFromCommandException($ex)
538 ->setPhase(self
::PHASE_SQUASHMERGE
)
539 ->setDisplayCommand($display_command);
541 $lease->setAttribute('workingcopy.vcs.error', $error->toDictionary());
546 public function getCommandError(DrydockLease
$lease) {
547 return $lease->getAttribute('workingcopy.vcs.error');
550 private function execxv(
551 DrydockCommandInterface
$interface,
554 return $this->newExecvFuture($interface, $commands, $arguments)->resolvex();
557 private function newExecvFuture(
558 DrydockCommandInterface
$interface,
562 $commands = implode(' && ', $commands);
563 $argv = array_merge(array($commands), $arguments);
565 return call_user_func_array(array($interface, 'getExecFuture'), $argv);