Path: blob/master/src/applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php
12256 views
<?php12final class DrydockWorkingCopyBlueprintImplementation3extends DrydockBlueprintImplementation {45const PHASE_SQUASHMERGE = 'squashmerge';6const PHASE_REMOTEFETCH = 'blueprint.workingcopy.fetch.remote';7const PHASE_MERGEFETCH = 'blueprint.workingcopy.fetch.staging';89public function isEnabled() {10return true;11}1213public function getBlueprintName() {14return pht('Working Copy');15}1617public function getBlueprintIcon() {18return 'fa-folder-open';19}2021public function getDescription() {22return pht('Allows Drydock to check out working copies of repositories.');23}2425public function canAnyBlueprintEverAllocateResourceForLease(26DrydockLease $lease) {27return true;28}2930public function canEverAllocateResourceForLease(31DrydockBlueprint $blueprint,32DrydockLease $lease) {33return true;34}3536public function canAllocateResourceForLease(37DrydockBlueprint $blueprint,38DrydockLease $lease) {39$viewer = $this->getViewer();4041if ($this->shouldLimitAllocatingPoolSize($blueprint)) {42return false;43}4445return true;46}4748public function canAcquireLeaseOnResource(49DrydockBlueprint $blueprint,50DrydockResource $resource,51DrydockLease $lease) {5253// Don't hand out leases on working copies which have not activated, since54// it may take an arbitrarily long time for them to acquire a host.55if (!$resource->isActive()) {56return false;57}5859$need_map = $lease->getAttribute('repositories.map');60if (!is_array($need_map)) {61return false;62}6364$have_map = $resource->getAttribute('repositories.map');65if (!is_array($have_map)) {66return false;67}6869$have_as = ipull($have_map, 'phid');70$need_as = ipull($need_map, 'phid');7172foreach ($need_as as $need_directory => $need_phid) {73if (empty($have_as[$need_directory])) {74// This resource is missing a required working copy.75return false;76}7778if ($have_as[$need_directory] != $need_phid) {79// This resource has a required working copy, but it contains80// the wrong repository.81return false;82}8384unset($have_as[$need_directory]);85}8687if ($have_as && $lease->getAttribute('repositories.strict')) {88// This resource has extra repositories, but the lease is strict about89// which repositories are allowed to exist.90return false;91}9293if (!DrydockSlotLock::isLockFree($this->getLeaseSlotLock($resource))) {94return false;95}9697return true;98}99100public function acquireLease(101DrydockBlueprint $blueprint,102DrydockResource $resource,103DrydockLease $lease) {104105$lease106->needSlotLock($this->getLeaseSlotLock($resource))107->acquireOnResource($resource);108}109110private function getLeaseSlotLock(DrydockResource $resource) {111$resource_phid = $resource->getPHID();112return "workingcopy.lease({$resource_phid})";113}114115public function allocateResource(116DrydockBlueprint $blueprint,117DrydockLease $lease) {118119$resource = $this->newResourceTemplate($blueprint);120121$resource_phid = $resource->getPHID();122123$blueprint_phids = $blueprint->getFieldValue('blueprintPHIDs');124125$host_lease = $this->newLease($blueprint)126->setResourceType('host')127->setOwnerPHID($resource_phid)128->setAttribute('workingcopy.resourcePHID', $resource_phid)129->setAllowedBlueprintPHIDs($blueprint_phids);130$resource->setAttribute('host.leasePHID', $host_lease->getPHID());131132$map = $this->getWorkingCopyRepositoryMap($lease);133$resource->setAttribute('repositories.map', $map);134135$slot_lock = $this->getConcurrentResourceLimitSlotLock($blueprint);136if ($slot_lock !== null) {137$resource->needSlotLock($slot_lock);138}139140$resource->allocateResource();141142$host_lease->queueForActivation();143144return $resource;145}146147private function getWorkingCopyRepositoryMap(DrydockLease $lease) {148$attribute = 'repositories.map';149$map = $lease->getAttribute($attribute);150151// TODO: Leases should validate their attributes more formally.152153if (!is_array($map) || !$map) {154$message = array();155if ($map === null) {156$message[] = pht(157'Working copy lease is missing required attribute "%s".',158$attribute);159} else {160$message[] = pht(161'Working copy lease has invalid attribute "%s".',162$attribute);163}164165$message[] = pht(166'Attribute "repositories.map" should be a map of repository '.167'specifications.');168169$message = implode("\n\n", $message);170171throw new Exception($message);172}173174foreach ($map as $key => $value) {175$map[$key] = array_select_keys(176$value,177array(178'phid',179));180}181182return $map;183}184185public function activateResource(186DrydockBlueprint $blueprint,187DrydockResource $resource) {188189$lease = $this->loadHostLease($resource);190$this->requireActiveLease($lease);191192$command_type = DrydockCommandInterface::INTERFACE_TYPE;193$interface = $lease->getInterface($command_type);194195// TODO: Make this configurable.196$resource_id = $resource->getID();197$root = "/var/drydock/workingcopy-{$resource_id}";198199$map = $resource->getAttribute('repositories.map');200201$futures = array();202$repositories = $this->loadRepositories(ipull($map, 'phid'));203foreach ($map as $directory => $spec) {204// TODO: Validate directory isn't goofy like "/etc" or "../../lol"205// somewhere?206207$repository = $repositories[$spec['phid']];208$path = "{$root}/repo/{$directory}/";209210$future = $interface->getExecFuture(211'git clone -- %s %s',212(string)$repository->getCloneURIObject(),213$path);214215$future->setTimeout($repository->getEffectiveCopyTimeLimit());216217$futures[$directory] = $future;218}219220foreach (new FutureIterator($futures) as $key => $future) {221$future->resolvex();222}223224$resource225->setAttribute('workingcopy.root', $root)226->activateResource();227}228229public function destroyResource(230DrydockBlueprint $blueprint,231DrydockResource $resource) {232233try {234$lease = $this->loadHostLease($resource);235} catch (Exception $ex) {236// If we can't load the lease, assume we don't need to take any actions237// to destroy it.238return;239}240241// Destroy the lease on the host.242$lease->setReleaseOnDestruction(true);243244if ($lease->isActive()) {245// Destroy the working copy on disk.246$command_type = DrydockCommandInterface::INTERFACE_TYPE;247$interface = $lease->getInterface($command_type);248249$root_key = 'workingcopy.root';250$root = $resource->getAttribute($root_key);251if (strlen($root)) {252$interface->execx('rm -rf -- %s', $root);253}254}255}256257public function getResourceName(258DrydockBlueprint $blueprint,259DrydockResource $resource) {260return pht('Working Copy');261}262263264public function activateLease(265DrydockBlueprint $blueprint,266DrydockResource $resource,267DrydockLease $lease) {268269$host_lease = $this->loadHostLease($resource);270$command_type = DrydockCommandInterface::INTERFACE_TYPE;271$interface = $host_lease->getInterface($command_type);272273$map = $lease->getAttribute('repositories.map');274$root = $resource->getAttribute('workingcopy.root');275276$repositories = $this->loadRepositories(ipull($map, 'phid'));277278$default = null;279foreach ($map as $directory => $spec) {280$repository = $repositories[$spec['phid']];281282$interface->pushWorkingDirectory("{$root}/repo/{$directory}/");283284$cmd = array();285$arg = array();286287$cmd[] = 'git clean -d --force';288$cmd[] = 'git fetch';289290$commit = idx($spec, 'commit');291$branch = idx($spec, 'branch');292293$ref = idx($spec, 'ref');294295// Reset things first, in case previous builds left anything staged or296// dirty. Note that we don't reset to "HEAD" because that does not work297// in empty repositories.298$cmd[] = 'git reset --hard';299300if ($commit !== null) {301$cmd[] = 'git checkout %s --';302$arg[] = $commit;303} else if ($branch !== null) {304$cmd[] = 'git checkout %s --';305$arg[] = $branch;306307$cmd[] = 'git reset --hard origin/%s';308$arg[] = $branch;309}310311$this->newExecvFuture($interface, $cmd, $arg)312->setTimeout($repository->getEffectiveCopyTimeLimit())313->resolvex();314315if (idx($spec, 'default')) {316$default = $directory;317}318319// If we're fetching a ref from a remote, do that separately so we can320// raise a more tailored error.321if ($ref) {322$cmd = array();323$arg = array();324325$ref_uri = $ref['uri'];326$ref_ref = $ref['ref'];327328$cmd[] = 'git fetch --no-tags -- %s +%s:%s';329$arg[] = $ref_uri;330$arg[] = $ref_ref;331$arg[] = $ref_ref;332333$cmd[] = 'git checkout %s --';334$arg[] = $ref_ref;335336try {337$this->newExecvFuture($interface, $cmd, $arg)338->setTimeout($repository->getEffectiveCopyTimeLimit())339->resolvex();340} catch (CommandException $ex) {341$display_command = csprintf(342'git fetch %R %R',343$ref_uri,344$ref_ref);345346$error = DrydockCommandError::newFromCommandException($ex)347->setPhase(self::PHASE_REMOTEFETCH)348->setDisplayCommand($display_command);349350$lease->setAttribute(351'workingcopy.vcs.error',352$error->toDictionary());353354throw $ex;355}356}357358$merges = idx($spec, 'merges');359if ($merges) {360foreach ($merges as $merge) {361$this->applyMerge($lease, $interface, $merge);362}363}364365$interface->popWorkingDirectory();366}367368if ($default === null) {369$default = head_key($map);370}371372// TODO: Use working storage?373$lease->setAttribute('workingcopy.default', "{$root}/repo/{$default}/");374375$lease->activateOnResource($resource);376}377378public function didReleaseLease(379DrydockBlueprint $blueprint,380DrydockResource $resource,381DrydockLease $lease) {382// We leave working copies around even if there are no leases on them,383// since the cost to maintain them is nearly zero but rebuilding them is384// moderately expensive and it's likely that they'll be reused.385return;386}387388public function destroyLease(389DrydockBlueprint $blueprint,390DrydockResource $resource,391DrydockLease $lease) {392// When we activate a lease we just reset the working copy state and do393// not create any new state, so we don't need to do anything special when394// destroying a lease.395return;396}397398public function getType() {399return 'working-copy';400}401402public function getInterface(403DrydockBlueprint $blueprint,404DrydockResource $resource,405DrydockLease $lease,406$type) {407408switch ($type) {409case DrydockCommandInterface::INTERFACE_TYPE:410$host_lease = $this->loadHostLease($resource);411$command_interface = $host_lease->getInterface($type);412413$path = $lease->getAttribute('workingcopy.default');414$command_interface->pushWorkingDirectory($path);415416return $command_interface;417}418}419420private function loadRepositories(array $phids) {421$viewer = $this->getViewer();422423$repositories = id(new PhabricatorRepositoryQuery())424->setViewer($viewer)425->withPHIDs($phids)426->execute();427$repositories = mpull($repositories, null, 'getPHID');428429foreach ($phids as $phid) {430if (empty($repositories[$phid])) {431throw new Exception(432pht(433'Repository PHID "%s" does not exist.',434$phid));435}436}437438foreach ($repositories as $repository) {439$repository_vcs = $repository->getVersionControlSystem();440switch ($repository_vcs) {441case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:442break;443default:444throw new Exception(445pht(446'Repository ("%s") has unsupported VCS ("%s").',447$repository->getPHID(),448$repository_vcs));449}450}451452return $repositories;453}454455private function loadHostLease(DrydockResource $resource) {456$viewer = $this->getViewer();457458$lease_phid = $resource->getAttribute('host.leasePHID');459460$lease = id(new DrydockLeaseQuery())461->setViewer($viewer)462->withPHIDs(array($lease_phid))463->executeOne();464if (!$lease) {465throw new Exception(466pht(467'Unable to load lease ("%s").',468$lease_phid));469}470471return $lease;472}473474protected function getCustomFieldSpecifications() {475return array(476'blueprintPHIDs' => array(477'name' => pht('Use Blueprints'),478'type' => 'blueprints',479'required' => true,480),481);482}483484protected function shouldUseConcurrentResourceLimit() {485return true;486}487488private function applyMerge(489DrydockLease $lease,490DrydockCommandInterface $interface,491array $merge) {492493$src_uri = $merge['src.uri'];494$src_ref = $merge['src.ref'];495496497try {498$interface->execx(499'git fetch --no-tags -- %s +%s:%s',500$src_uri,501$src_ref,502$src_ref);503} catch (CommandException $ex) {504$display_command = csprintf(505'git fetch %R +%R:%R',506$src_uri,507$src_ref,508$src_ref);509510$error = DrydockCommandError::newFromCommandException($ex)511->setPhase(self::PHASE_MERGEFETCH)512->setDisplayCommand($display_command);513514$lease->setAttribute('workingcopy.vcs.error', $error->toDictionary());515516throw $ex;517}518519520// NOTE: This can never actually generate a commit because we pass521// "--squash", but git sometimes runs code to check that a username and522// email are configured anyway.523$real_command = csprintf(524'git -c user.name=%s -c user.email=%s merge --no-stat --squash -- %R',525'drydock',526'drydock@phabricator',527$src_ref);528529try {530$interface->execx('%C', $real_command);531} catch (CommandException $ex) {532$display_command = csprintf(533'git merge --squash %R',534$src_ref);535536$error = DrydockCommandError::newFromCommandException($ex)537->setPhase(self::PHASE_SQUASHMERGE)538->setDisplayCommand($display_command);539540$lease->setAttribute('workingcopy.vcs.error', $error->toDictionary());541throw $ex;542}543}544545public function getCommandError(DrydockLease $lease) {546return $lease->getAttribute('workingcopy.vcs.error');547}548549private function execxv(550DrydockCommandInterface $interface,551array $commands,552array $arguments) {553return $this->newExecvFuture($interface, $commands, $arguments)->resolvex();554}555556private function newExecvFuture(557DrydockCommandInterface $interface,558array $commands,559array $arguments) {560561$commands = implode(' && ', $commands);562$argv = array_merge(array($commands), $arguments);563564return call_user_func_array(array($interface, 'getExecFuture'), $argv);565}566567}568569570