Page MenuHomec4science

PhabricatorJenkinsJobController.php
No OneTemporary

File Metadata

Created
Sun, Dec 22, 23:45

PhabricatorJenkinsJobController.php

<?php
final class PhabricatorJenkinsJobController extends PhabricatorController {
protected function buildSideNav() {
$nav = new AphrontSideNavFilterView();
$nav->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 .= <<<EOF
<permission>hudson.model.Item.Cancel:$p</permission>
<permission>hudson.model.Item.Read:$p</permission>
<permission>hudson.model.Run.Delete:$p</permission>
<permission>hudson.model.Item.Build:$p</permission>
<permission>hudson.model.Run.Replay:$p</permission>
<permission>hudson.model.Item.ViewStatus:$p</permission>
<permission>hudson.model.Run.Update:$p</permission>
EOF;
}
$job_xml = <<<EOF
<?xml version='1.0' encoding='UTF-8'?>
<flow-definition plugin="workflow-job@2.10">
<actions/>
<description></description>
<keepDependencies>false</keepDependencies>
<properties>
<hudson.security.AuthorizationMatrixProperty>
$proj_xml
<permission>hudson.model.Item.Cancel:$username</permission>
<permission>hudson.model.Item.Read:$username</permission>
<permission>hudson.model.Run.Delete:$username</permission>
<permission>hudson.model.Item.Build:$username</permission>
<permission>hudson.model.Run.Replay:$username</permission>
<permission>hudson.model.Item.ViewStatus:$username</permission>
<permission>hudson.model.Run.Update:$username</permission>
</hudson.security.AuthorizationMatrixProperty>
<!--<hudson.model.ParametersDefinitionProperty>
<parameterDefinitions>
<hudson.model.StringParameterDefinition>
<name>PHID</name>
<description/>
<defaultValue/>
</hudson.model.StringParameterDefinition>
<hudson.model.StringParameterDefinition>
<name>DIFF_ID</name>
<description/>
<defaultValue/>
</hudson.model.StringParameterDefinition>
</parameterDefinitions>
</hudson.model.ParametersDefinitionProperty>
-->
<org.jenkinsci.plugins.workflow.job.properties.PipelineTriggersJobProperty>
<triggers/>
</org.jenkinsci.plugins.workflow.job.properties.PipelineTriggersJobProperty>
</properties>
<definition class="org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition" plugin="workflow-cps@2.30">
<scm class="hudson.plugins.git.GitSCM" plugin="git@3.3.0">
<configVersion>2</configVersion>
<userRemoteConfigs>
<hudson.plugins.git.UserRemoteConfig>
<url>$repo_uri</url>
<credentialsId>$repo_cred</credentialsId>
</hudson.plugins.git.UserRemoteConfig>
</userRemoteConfigs>
<branches>
<hudson.plugins.git.BranchSpec>
<name>refs/heads/$branch</name>
</hudson.plugins.git.BranchSpec>
</branches>
<doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
<browser class="hudson.plugins.git.browser.Phabricator">
<url>$repo_browse</url>
<repo>$repo_name</repo>
</browser>
<submoduleCfg class="list"/>
<extensions/>
</scm>
<scriptPath>Jenkinsfile</scriptPath>
<lightweight>true</lightweight>
</definition>
<triggers/>
<authToken>$job_token</authToken>
</flow-definition>
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)
->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)
->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 = <<<EOF
Create a Pipeline Job on c4science Jenkins instance.
- The Project will be used as Policy access to the job.
- The Repository and Branch will be checked out in the job workspace.
- The Repository must contain a Jenkinsfile at the root, see [[https://jenkins.io/doc/book/pipeline/jenkinsfile/ | Using a Jenkinsfile]]
EOF;
$dialog = id(new AphrontDialogView())
->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);
}
}

Event Timeline