Index: src/__phutil_library_map__.php =================================================================== --- src/__phutil_library_map__.php +++ src/__phutil_library_map__.php @@ -642,6 +642,7 @@ 'DrydockConstants' => 'applications/drydock/constants/DrydockConstants.php', 'DrydockController' => 'applications/drydock/controller/DrydockController.php', 'DrydockDAO' => 'applications/drydock/storage/DrydockDAO.php', + 'DrydockFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockFilesystemInterface.php', 'DrydockInterface' => 'applications/drydock/interface/DrydockInterface.php', 'DrydockLease' => 'applications/drydock/storage/DrydockLease.php', 'DrydockLeaseListController' => 'applications/drydock/controller/DrydockLeaseListController.php', @@ -668,6 +669,7 @@ 'DrydockResourceQuery' => 'applications/drydock/query/DrydockResourceQuery.php', 'DrydockResourceStatus' => 'applications/drydock/constants/DrydockResourceStatus.php', 'DrydockResourceViewController' => 'applications/drydock/controller/DrydockResourceViewController.php', + 'DrydockSFTPFilesystemInterface' => 'applications/drydock/interface/filesystem/DrydockSFTPFilesystemInterface.php', 'DrydockSSHCommandInterface' => 'applications/drydock/interface/command/DrydockSSHCommandInterface.php', 'DrydockWebrootInterface' => 'applications/drydock/interface/webroot/DrydockWebrootInterface.php', 'DrydockWorkingCopyBlueprintImplementation' => 'applications/drydock/blueprint/DrydockWorkingCopyBlueprintImplementation.php', @@ -2332,6 +2334,7 @@ 'SleepBuildStepImplementation' => 'applications/harbormaster/step/SleepBuildStepImplementation.php', 'SlowvoteEmbedView' => 'applications/slowvote/view/SlowvoteEmbedView.php', 'SlowvoteRemarkupRule' => 'applications/slowvote/remarkup/SlowvoteRemarkupRule.php', + 'UploadArtifactBuildStepImplementation' => 'applications/harbormaster/step/UploadArtifactBuildStepImplementation.php', 'VariableBuildStepImplementation' => 'applications/harbormaster/step/VariableBuildStepImplementation.php', ), 'function' => @@ -2987,6 +2990,7 @@ 'DrydockCommandInterface' => 'DrydockInterface', 'DrydockController' => 'PhabricatorController', 'DrydockDAO' => 'PhabricatorLiskDAO', + 'DrydockFilesystemInterface' => 'DrydockInterface', 'DrydockLease' => 'DrydockDAO', 'DrydockLeaseListController' => 'DrydockController', 'DrydockLeaseQuery' => 'PhabricatorOffsetPagedQuery', @@ -3016,6 +3020,7 @@ 'DrydockResourceQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'DrydockResourceStatus' => 'DrydockConstants', 'DrydockResourceViewController' => 'DrydockController', + 'DrydockSFTPFilesystemInterface' => 'DrydockFilesystemInterface', 'DrydockSSHCommandInterface' => 'DrydockCommandInterface', 'DrydockWebrootInterface' => 'DrydockInterface', 'DrydockWorkingCopyBlueprintImplementation' => 'DrydockBlueprintImplementation', @@ -4973,6 +4978,7 @@ 'SleepBuildStepImplementation' => 'BuildStepImplementation', 'SlowvoteEmbedView' => 'AphrontView', 'SlowvoteRemarkupRule' => 'PhabricatorRemarkupRuleObject', + 'UploadArtifactBuildStepImplementation' => 'VariableBuildStepImplementation', 'VariableBuildStepImplementation' => 'BuildStepImplementation', ), )); Index: src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php =================================================================== --- src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php +++ src/applications/drydock/blueprint/DrydockPreallocatedHostBlueprintImplementation.php @@ -105,6 +105,12 @@ 'port' => $resource->getAttribute('port'), 'credential' => $resource->getAttribute('credential'), 'platform' => $resource->getAttribute('platform'))); + case 'filesystem': + return id(new DrydockSFTPFilesystemInterface()) + ->setConfiguration(array( + 'host' => $resource->getAttribute('host'), + 'port' => $resource->getAttribute('port'), + 'credential' => $resource->getAttribute('credential'))); } throw new Exception("No interface of type '{$type}'."); Index: src/applications/drydock/interface/filesystem/DrydockFilesystemInterface.php =================================================================== --- /dev/null +++ src/applications/drydock/interface/filesystem/DrydockFilesystemInterface.php @@ -0,0 +1,24 @@ +passphraseSSHKey !== null) { + return; + } + + $credential = id(new PassphraseCredentialQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withIDs(array($this->getConfig('credential'))) + ->needSecrets(true) + ->executeOne(); + + if ($credential->getProvidesType() !== + PassphraseCredentialTypeSSHPrivateKey::PROVIDES_TYPE) { + throw new Exception("Only private key credentials are supported."); + } + + $this->passphraseSSHKey = PassphraseSSHKey::loadFromPHID( + $credential->getPHID(), + PhabricatorUser::getOmnipotentUser()); + } + + private function getExecFuture($path) { + $this->openCredentialsIfNotOpen(); + + return new ExecFuture( + 'sftp -o "StrictHostKeyChecking no" -P %s -i %P %P@%s', + $this->getConfig('port'), + $this->passphraseSSHKey->getKeyfileEnvelope(), + $this->passphraseSSHKey->getUsernameEnvelope(), + $this->getConfig('host')); + } + + public function readFile($path) { + $target = new TempFile(); + $future = $this->getExecFuture($path); + $future->write(csprintf("get %s %s", $path, $target)); + $future->resolvex(); + return Filesystem::readFile($target); + } + + public function saveFile($path, $name) { + $data = $this->readFile($path); + $file = PhabricatorFile::newFromFileData($data); + $file->setName($name); + $file->save(); + return $file; + } + + public function writeFile($path, $data) { + $source = new TempFile(); + Filesystem::writeFile($source, $data); + $future = $this->getExecFuture($path); + $future->write(csprintf("put %s %s", $source, $path)); + $future->resolvex(); + } + +} Index: src/applications/drydock/query/DrydockBlueprintQuery.php =================================================================== --- src/applications/drydock/query/DrydockBlueprintQuery.php +++ src/applications/drydock/query/DrydockBlueprintQuery.php @@ -56,7 +56,7 @@ if ($this->phids) { $where[] = qsprintf( $conn_r, - 'phid IN (%Ld)', + 'phid IN (%Ls)', $this->phids); } Index: src/applications/harbormaster/step/CommandBuildStepImplementation.php =================================================================== --- src/applications/harbormaster/step/CommandBuildStepImplementation.php +++ src/applications/harbormaster/step/CommandBuildStepImplementation.php @@ -32,30 +32,9 @@ $settings['command'], $variables); - $artifact = id(new HarbormasterBuildArtifactQuery()) - ->setViewer(PhabricatorUser::getOmnipotentUser()) - ->withArtifactKeys( - $build->getPHID(), - array($settings['hostartifact'])) - ->executeOne(); - if ($artifact === null) { - throw new Exception("Associated Drydock host artifact not found!"); - } - - $data = $artifact->getArtifactData(); + $artifact = $build->loadArtifact($settings['hostartifact']); - // FIXME: Is there a better way of doing this? - $lease = id(new DrydockLease())->load( - $data['drydock-lease']); - if ($lease === null) { - throw new Exception("Associated Drydock lease not found!"); - } - $resource = id(new DrydockResource())->load( - $lease->getResourceID()); - if ($resource === null) { - throw new Exception("Associated Drydock resource not found!"); - } - $lease->attachResource($resource); + $lease = $artifact->loadDrydockLease(); $interface = $lease->getInterface('command'); Index: src/applications/harbormaster/step/UploadArtifactBuildStepImplementation.php =================================================================== --- /dev/null +++ src/applications/harbormaster/step/UploadArtifactBuildStepImplementation.php @@ -0,0 +1,97 @@ +getSettings(); + + return pht( + 'Upload artifact located at \'%s\' on \'%s\'.', + $settings['path'], + $settings['hostartifact']); + } + + public function execute( + HarbormasterBuild $build, + HarbormasterBuildTarget $build_target) { + + $settings = $this->getSettings(); + $variables = $build_target->getVariables(); + + $path = $this->mergeVariables( + 'vsprintf', + $settings['path'], + $variables); + + $artifact = $build->loadArtifact($settings['hostartifact']); + + $lease = $artifact->loadDrydockLease(); + + $interface = $lease->getInterface('filesystem'); + + // TODO: Handle exceptions. + $file = $interface->saveFile($path, $settings['name']); + + // Insert the artifact record. + $artifact = $build->createArtifact( + $build_target, + $settings['name'], + HarbormasterBuildArtifact::TYPE_FILE); + $artifact->setArtifactData(array( + 'filePHID' => $file->getPHID())); + $artifact->save(); + } + + public function validateSettings() { + $settings = $this->getSettings(); + + if ($settings['path'] === null || !is_string($settings['path'])) { + return false; + } + if ($settings['name'] === null || !is_string($settings['name'])) { + return false; + } + if ($settings['hostartifact'] === null || + !is_string($settings['hostartifact'])) { + return false; + } + + // TODO: Check if the host artifact is provided by previous build steps. + + return true; + } + + public function getSettingDefinitions() { + return array( + 'path' => array( + 'name' => 'Path', + 'description' => + 'The path of the file that should be retrieved. Note that on '. + 'Windows machines running FreeSSHD, this path will be relative '. + 'to the SFTP root path (configured under the SFTP tab). You can '. + 'not specify an absolute path for Windows machines.', + 'type' => BuildStepImplementation::SETTING_TYPE_STRING), + 'name' => array( + 'name' => 'Local Name', + 'description' => + 'The name for the file when it is stored in Phabricator.', + 'type' => BuildStepImplementation::SETTING_TYPE_STRING), + 'hostartifact' => array( + 'name' => 'Host Artifact', + 'description' => + 'The host artifact that determines what machine the command '. + 'will run on.', + 'type' => BuildStepImplementation::SETTING_TYPE_ARTIFACT, + 'artifact_type' => HarbormasterBuildArtifact::TYPE_HOST)); + } + +} Index: src/applications/harbormaster/storage/build/HarbormasterBuild.php =================================================================== --- src/applications/harbormaster/storage/build/HarbormasterBuild.php +++ src/applications/harbormaster/storage/build/HarbormasterBuild.php @@ -119,6 +119,19 @@ return $artifact; } + public function loadArtifact($name) { + $artifact = id(new HarbormasterBuildArtifactQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withArtifactKeys( + $this->getPHID(), + array($name)) + ->executeOne(); + if ($artifact === null) { + throw new Exception("Artifact not found!"); + } + return $artifact; + } + /** * Checks for and handles build cancellation. If this method returns * true, the caller should stop any current operations and return control Index: src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php =================================================================== --- src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php +++ src/applications/harbormaster/storage/build/HarbormasterBuildArtifact.php @@ -72,6 +72,30 @@ } } + public function loadDrydockLease() { + if ($this->getArtifactType() !== self::TYPE_HOST) { + throw new Exception( + "`loadDrydockLease` may only be called on host artifacts."); + } + + $data = $this->getArtifactData(); + + // FIXME: Is there a better way of doing this? + $lease = id(new DrydockLease())->load( + $data['drydock-lease']); + if ($lease === null) { + throw new Exception("Associated Drydock lease not found!"); + } + $resource = id(new DrydockResource())->load( + $lease->getResourceID()); + if ($resource === null) { + throw new Exception("Associated Drydock resource not found!"); + } + $lease->attachResource($resource); + + return $lease; + } + /* -( PhabricatorPolicyInterface )----------------------------------------- */