diff --git a/src/extensions/PhabricatorJenkinsJobController.php b/src/extensions/PhabricatorJenkinsJobController.php index c12d777e9..b98916d0f 100644 --- a/src/extensions/PhabricatorJenkinsJobController.php +++ b/src/extensions/PhabricatorJenkinsJobController.php @@ -1,720 +1,722 @@ setBaseURI(new PhutilURI('/jobs/')); $nav->addLabel(pht('Jenkins Jobs')); $nav->addFilter('list', 'List Jobs'); $nav->addFilter('create', 'Create Job'); $nav->selectFilter(null); return $nav; } public function buildApplicationMenu() { return $this->buildSideNav()->getMenu(); } protected function buildApplicationCrumbs() { $crumbs = parent::buildApplicationCrumbs(); return $crumbs; } public function shouldAllowPublic() { return false; } public function handleRequest(AphrontRequest $request) { if ($request->getPath() == '/jobs/create/') { return $this->handleCreate($request); } else if (strpos($request->getPath(), '/jobs/delete/') === 0) { return $this->handleDelete($request); } return $this->handleList($request); } private static function createJob($repo, $proj, $branch, $viewer) { $errors = array(); if (!empty($repo) && !empty($proj) && !empty($branch)) { $repo = head($repo); // Get Repository information $repo_query = id(new PhabricatorRepositoryQuery()) ->withPHIDs(array($repo)) ->setViewer($viewer) ->needURIs(true) ->executeOne(); if (!$repo_query) { $errors[] = "Repository '$repo' doesn't exist."; return $errors; } $repo_name = $repo_query->getCloneName(); $repo_id = $repo_query->getID(); $repo_browse = $repo_query->getURI(); $job_name = self::getJobName( $repo_name, $repo_id, $branch); $job_token = hash('sha256', microtime(true).mt_rand()); $username = $viewer->getUsername(); if (!$repo_query->isGit()) { $errors[] = "'$repo_name' is not a GIT Repository."; return $errors; } // Check branch $branches = DiffusionQuery::callConduitWithDiffusionRequest( $viewer, DiffusionRequest::newFromDictionary( array( 'repository' => $repo_query, 'user' => $viewer, )), 'diffusion.branchquery', array( 'repository' => $repo_query->getPHID(), )); $branches = ipull($branches, 'shortName'); if (!in_array($branch, $branches)) { $errors[] = "Branch '$branch' doesn't exist for Repository '$repo_name'."; return $errors; } // Get URI $display_never = PhabricatorRepositoryURI::DISPLAY_NEVER; $repo_cred = PhabricatorEnv::getEnvConfig('jenkins.repo_cred'); $repo_uri = ''; foreach ($repo_query->getURIs() as $uri) { if ($uri->getIsDisabled() || $uri->getEffectiveDisplayType() == $display_never) { continue; } if (strpos($uri->getDisplayURI(), 'ssh://') === 0) { $repo_uri = (string)$uri->getDisplayURI(); break; } } if (empty($repo_uri)) { $errors[] = 'Repository has no URI.'; return $errors; } // Jenkins Job configuration $proj_xml = ''; foreach ($proj as $p) { $proj_xml .= <<hudson.model.Item.Cancel:$p hudson.model.Item.Read:$p hudson.model.Run.Delete:$p hudson.model.Item.Build:$p hudson.model.Run.Replay:$p hudson.model.Item.ViewStatus:$p hudson.model.Run.Update:$p EOF; } $job_xml = << false $proj_xml hudson.model.Item.Cancel:$username hudson.model.Item.Read:$username hudson.model.Run.Delete:$username hudson.model.Item.Build:$username hudson.model.Run.Replay:$username hudson.model.Item.ViewStatus:$username hudson.model.Run.Update:$username 2 $repo_uri $repo_cred refs/heads/$branch false $repo_browse $repo_name Jenkinsfile true $job_token EOF; $jenkins_user = PhabricatorEnv::getEnvConfig('jenkins.user'); $jenkins_token = new PhutilOpaqueEnvelope( PhabricatorEnv::getEnvConfig('jenkins.token')); $jenkins_url = PhabricatorEnv::getEnvConfig('jenkins.url'); // Get CSRF cookie from Jenkins $csrf = self::getJenkinsCsrf( $jenkins_url, $jenkins_user, $jenkins_token); // Check if Job already exists $url = $jenkins_url."/checkJobName?value=$job_name"; $future = id(new HTTPSFuture($url)) ->addHeader($csrf[0], $csrf[1]) ->setHTTPBasicAuthCredentials($jenkins_user, $jenkins_token); list($status, $body, $headers) = $future->resolve(); if (strpos($body, 'job already exists') !== false) { $errors[] = "A job already exists with the name '$job_name'."; return $errors; } if ($status->getStatusCode() !== 200) { $errors[] = phutil_tag($status->getMessage()); return $errors; } // Create Jenkins Job $url = $jenkins_url."/createItem?name=$job_name"; $future = id(new HTTPSFuture($url)) ->setData($job_xml) ->addHeader('Content-Type', 'text/xml') ->addHeader($csrf[0], $csrf[1]) ->setHTTPBasicAuthCredentials($jenkins_user, $jenkins_token) ->setMethod('POST'); list($status, $body, $headers) = $future->resolve(); if ($status->getStatusCode() !== 200) { $errors[] = phutil_tag($status->getMessage()); return $errors; } // Custom policy $rules = array( array( 'action' => PhabricatorPolicy::ACTION_ALLOW, 'rule' => 'PhabricatorProjectsPolicyRule', 'value' => $proj, ) ); $policy = id(new PhabricatorPolicy()) ->setRules($rules) ->save(); // Create Harbormaster Build Plan $plan = HarbormasterBuildPlan::initializeNewBuildPlan($viewer) ->setName($job_name.' on jenkins') ->setViewPolicy($policy->getPHID()) ->setEditPolicy($policy->getPHID()) ->save(); $project_type = PhabricatorProjectObjectHasProjectEdgeType::EDGECONST; $xactions = array(); $xactions[] = id(new HarbormasterBuildPlanTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $project_type) ->setNewValue(array('=' => array_fuse($proj))); $editor = id(new HarbormasterBuildPlanEditor()) ->setActor($viewer) ->setContentSource(PhabricatorContentSource::newForSource('web')) ->applyTransactions($plan, $xactions); $step = HarbormasterBuildStep::initializeNewStep($viewer) ->setBuildPlanPHID($plan->getPHID()) ->setClassName('HarbormasterHTTPRequestBuildStepImplementation') // ->setDetail('builtin.wait-for-message', 'wait') ->setDetail('method', 'GET') ->setDetail('uri', $jenkins_url. "/buildByToken/build?job=${job_name}&token=${job_token}") // ->setDetail('uri', $jenkins_url . // '/buildByToken/buildWithParameters?' . // 'DIFF_ID=\${buildable.diff}&' . // 'PHID=\${target.phid}&' . // "token=$job_token&job=$job_name") ->setName('Start Job on Jenkins') ->save(); // Create Herald Rule $herald = id(new HeraldRule()) ->setMustMatchAll(true) ->setAuthorPHID($viewer->getPHID()) ->setRuleType(HeraldRuleTypeConfig::RULE_TYPE_OBJECT) ->setRepetitionPolicy(1) ->setTriggerObjectPHID($repo) ->setContentType('commit') ->setName("Run Jenkins Job $job_name on new commit") ->save(); $branch_escaped = str_replace('/', '\\/', $branch); $conditions = array( id(new HeraldCondition()) ->setFieldName('diffusion.commit.repository') ->setFieldCondition('isany') ->setValue(array($repo)) ->setRuleID($herald->getID()) ->save(), id(new HeraldCondition()) ->setFieldName('diffusion.commit.branches') ->setFieldCondition('regexp') ->setValue("/^${branch_escaped}$/") ->setRuleID($herald->getID()) ->save(), ); $herald->attachConditions($conditions); $actions = array( id(new HeraldActionRecord()) ->setAction('harbormaster.build') ->setTarget(array($plan->getPHID())) ->setRuleID($herald->getID()) ->save(), ); $herald->attachActions($actions); // Create Job object $job = id(new PhabricatorJenkinsJob()) ->setRepository($repo) ->setProject(implode(',', $proj)) ->setBuildPlan($plan->getPHID()) ->setRule($herald->getPHID()) ->setViewPolicy($policy->getPHID()) ->setEditPolicy($policy->getPHID()) ->setOwnerPHID($viewer->getPHID()) ->setBranch($branch); try { $job->save(); } catch (AphrontDuplicateKeyQueryException $ex) { $errors[] = pht('Only one job per repository and branch is allowed.'); self::deleteJenkinsJob( $job_name, $viewer); self::disableHerald($herald, $viewer); self::disableBuildPlan($plan, $viewer); } } else { if (empty($repo)) { $errors[] = pht('You must specify a Repository.'); } if (empty($proj)) { $errors[] = pht('You must specify a Project.'); } if (empty($branch)) { $errors[] = pht('You must specify a Branch.'); } } return $errors; } private static function deleteJenkinsJob( $job_name, $viewer) { $jenkins_url = PhabricatorEnv::getEnvConfig('jenkins.url'); $jenkins_user = PhabricatorEnv::getEnvConfig('jenkins.user'); $jenkins_token = new PhutilOpaqueEnvelope( PhabricatorEnv::getEnvConfig('jenkins.token')); $csrf = self::getJenkinsCsrf( $jenkins_url, $jenkins_user, $jenkins_token); $url = $jenkins_url."/job/$job_name/doDelete"; $future = id(new HTTPSFuture($url)) ->addHeader($csrf[0], $csrf[1]) ->setHTTPBasicAuthCredentials($jenkins_user, $jenkins_token) ->setMethod('POST'); list($status, $body, $headers) = $future->resolve(); } private static function disableHerald($herald, $viewer) { $xaction = id(new HeraldRuleTransaction()) ->setTransactionType(HeraldRuleTransaction::TYPE_DISABLE) ->setNewValue(true); id(new HeraldRuleEditor()) ->setActor($viewer) + ->setContinueOnNoEffect(true) ->setContentSource(PhabricatorContentSource::newForSource('web')) ->applyTransactions($herald, array($xaction)); } private function disableBuildPlan($plan, $viewer) { $xaction = id(new HarbormasterBuildPlanTransaction()) ->setTransactionType(HarbormasterBuildPlanTransaction::TYPE_STATUS) ->setNewValue(HarbormasterBuildPlan::STATUS_DISABLED); id(new HarbormasterBuildPlanEditor()) ->setActor($viewer) + ->setContinueOnNoEffect(true) ->setContentSource(PhabricatorContentSource::newForSource('web')) ->applyTransactions($plan, array($xaction)); } private function handleDelete(AphrontRequest $request) { $errors = array(); $text = ''; $viewer = $request->getViewer(); $job_id = $request->getInt('job', -1); $job = id(new PhabricatorJenkinsJobQuery()) ->setViewer($viewer) ->withIDs(array($job_id)) ->executeOne(); if (!$job) { $errors[] = "Job with id $job_id doesn't exist."; } else { // Show dialog $repo = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withPHIDs(array($job->getRepository())) ->executeOne(); $job_name = self::getJobName( $repo->getCloneName(), $repo->getID(), $job->getBranch()); $plan_phid = $job->getBuildPlan(); $plan = id(new HarbormasterBuildPlanQuery()) ->setViewer($viewer) ->withPHIDs(array($plan_phid)) ->executeOne(); $herald_phid = $job->getRule(); $herald = id(new HeraldRuleQuery()) ->setViewer($viewer) ->withPHIDs(array($herald_phid)) ->executeOne(); $text = "Are you sure you want to delete the Job '$job_name' ?"; // Confirm and delete if ($request->isFormPost()) { self::deleteJenkinsJob( $job_name, $viewer); self::disableHerald($herald, $viewer); self::disableBuildPlan($plan, $viewer); $job->delete(); return id(new AphrontRedirectResponse())->setURI('/jobs/list/'); } } $dialog = id(new AphrontDialogView()) ->setUser($viewer) ->setErrors($errors) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle(pht('Delete Jenkins Job')) ->appendChild($text) ->addCancelButton('/jobs'); if (empty($errors)) { $dialog->addSubmitButton(pht('Delete')); } return $dialog; } private function handleCreate(AphrontRequest $request) { $project = $request->getStr('with_project', null); $dialog = self::createForm($request, $project); return $dialog; } public static function createForm($request, $with_project) { $viewer = $request->getViewer(); $errors = array(); $repo = array(); $proj = array(); $branch = 'master'; if ($request->isFormPost()) { // Get form values $repo = $request->getArr('repository', array()); $proj = $request->getArr('project', array($with_project)); $branch = $request->getStr('branch', 'master'); // Create the Job $errors = self::createJob( $repo, $proj, $branch, $viewer); if (!$errors) { return id(new AphrontRedirectResponse())->setURI('/jobs/list/'); } } $jobs = id(new PhabricatorJenkinsJobQuery()) ->setViewer($viewer) ->execute(); $form = id(new AphrontFormView()) ->setUser($viewer); $form_proj = id(new PhabricatorDatasourceEditField()) ->setKey('project') ->setLabel(pht('Project')) ->setValue($proj) ->setIsRequired(true) ->setDatasource(new PhabricatorProjectDatasource()); if (!empty($with_project)) { $form_proj->setIsHidden(true); } $form_proj->appendToForm($form); $form_repo = id(new PhabricatorDatasourceEditField()) ->setKey('repository') ->setLabel(pht('Repository')) ->setValue($repo) ->setIsRequired(true) ->setDatasource(new DiffusionRepositoryDatasource()); $form_repo->appendToForm($form); $form_branch = id(new PhabricatorTextEditField()) ->setKey('branch') ->setLabel('Branch') ->setValue($branch) ->setIsRequired(true); $form_branch->appendToForm($form); $dialog_text = <<setErrors($errors) ->setUser($viewer) ->setWidth(AphrontDialogView::WIDTH_FORM) ->setTitle(pht('Create Jenkins Job')) ->appendChild(new PHUIRemarkupView($viewer, $dialog_text)) ->appendParagraph('') ->appendChild($form->buildLayoutView()) ->addSubmitButton(pht('Create')) ->addCancelButton('/jobs'); return $dialog; } private function handleList(AphrontRequest $request) { $viewer = $request->getViewer(); $title = pht('Jenkins Jobs List'); $list = self::createObjectList( $viewer, null, null); $box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setObjectList($list); $nav = $this->buildSideNav(); $nav->setCrumbs( $this->buildApplicationCrumbs() ->addTextCrumb($title)); $nav->appendChild($box); $nav->selectFilter('list'); return $this->newPage() ->setTitle($title) ->appendChild($nav); } public static function createObjectList( PhabricatorUser $viewer, $with_project, $with_repo) { $jenkins_url = PhabricatorEnv::getEnvConfig('jenkins.url'); $jenkins_user = PhabricatorEnv::getEnvConfig('jenkins.user'); $jenkins_token = new PhutilOpaqueEnvelope( PhabricatorEnv::getEnvConfig('jenkins.token')); $csrf = self::getJenkinsCsrf( $jenkins_url, $jenkins_user, $jenkins_token); // Get jobs $jobs = id(new PhabricatorJenkinsJobQuery()) ->setViewer($viewer); if (!empty($with_project)) { $jobs->withProjects(array($with_project)); } if (!empty($with_repo)) { $jobs->withRepositories(array($with_repo)); } $jobs = $jobs->execute(); $list = new PHUIObjectItemListView(); $list->setNoDataString('No Jenkins Job. Create a Job from a Project for this Repository.'); foreach ($jobs as $job) { // Get repo and project information $repo = id(new PhabricatorRepositoryQuery()) ->setViewer($viewer) ->withPHIDs(array($job->getRepository())) ->executeOne(); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withPHIDs(explode(',', $job->getProject())) ->execute(); $job_name = self::getJobName( $repo->getCloneName(), $repo->getID(), $job->getBranch()); // Display $item = id(new PHUIObjectItemView()); $item->setHeader($job_name); // $item->setHref( // $jenkins_url . // '/blue/organizations/jenkins/' . // $job_name . '/activity'); $item->setHref("${jenkins_url}/job/${job_name}/"); if (empty($with_repo)) { $item->addAttribute(id(new PHUITagView()) ->setType(PHUITagView::TYPE_OBJECT) ->setHref($repo->getURI().'repository/'.$job->getBranch().'/') ->setName($repo->getMonogram().' '.$repo->getName())); } $item->addAttribute($job->getBranch()); if (empty($with_project)) { foreach ($project as $p) { $item->addAttribute(id(new PHUITagView()) ->setType(PHUITagView::TYPE_OBJECT) ->setHref($p->getURI()) ->setName($p->getName())); } } if ($viewer->isOmnipotent()) { $user = id(new PhabricatorPeopleQuery()) ->setViewer($viewer) ->withPHIDs(array($job->getOwnerPHID())) ->executeOne(); if ($user) { $item->addAttribute($user->getUsername()); } } $item->addAction( id(new PHUIListItemView()) ->setHref('/jobs/delete/?job='.$job->getID()) ->setWorkflow(true) ->setIcon('fa-times')); $list->addItem($item); // Get Job status $url = $jenkins_url."/job/${job_name}/lastBuild/api/json"; $future = id(new HTTPSFuture($url)) ->addHeader($csrf[0], $csrf[1]) ->setHTTPBasicAuthCredentials($jenkins_user, $jenkins_token); list($status, $body, $headers) = $future->resolve(); if ($status->getStatusCode() == 200) { $json = json_decode($body, true); $result = $json['result']; $building = $json['building']; $icon = 'fa-question-circle'; $text = 'unknown'; if ($building) { $icon = 'fa-cogs'; $text = 'building'; } else { if ($result == 'SUCCESS') { $icon = 'fa-sun-o'; $text = 'success'; } else if ($result == 'FAILURE') { $icon = 'fa-bolt'; $text = 'failure'; } } $item->setStatusIcon($icon, $text); } } return $list; } private static function getJobName($repo_name, $repo_id, $branch) { // phutil_escape_uri() doen't escape / $branch = str_replace('/', '-', $branch); return phutil_escape_uri("${repo_name}-${branch}-${repo_id}"); } private static function getJenkinsCsrf( $jenkins_url, $jenkins_user, $jenkins_token) { $url = $jenkins_url.'/crumbIssuer/api/xml?xpath=concat(//crumbRequestField,":",//crumb)'; list($status, $csrf, $headers) = id(new HTTPSFuture($url)) ->setHTTPBasicAuthCredentials($jenkins_user, $jenkins_token) ->resolve(); if ($status->getStatusCode() !== 200) { return array(0, 0); } return explode(':', $csrf); } }