1 /* 2 Purpose : Scan Jenkins jobs 3 Author : Ky-Anh Huynh 4 License : MIT 5 Date : 2017-10-xx 6 */ 7 8 module dusybox.jenkins.utils; 9 10 import std.net.curl; 11 import std.json; 12 import std.stdio; 13 import std.algorithm.iteration; 14 import std.array; 15 import std.string; 16 17 /* 18 Getting from function arguments 19 Getting from environment 20 Getting from file (if any) 21 */ 22 string[] getJenkinsAuthenticationFromEnv(in string envName = "JENKINS_TOKEN", in string envValue = null) { 23 24 string token; 25 if (envValue is null) { 26 import std.process; 27 token = std.process.environment.get(envName); 28 } 29 else { 30 token = envValue; 31 } 32 33 if (token is null && envName == "JENKINS_TOKEN") { 34 import std.file; 35 import std.path; 36 try { 37 // FIXME: Use dotEnv instead. 38 auto token_file = chainPath("~", ".jenkins.token").array.expandTilde; 39 token = readText(token_file).strip(); 40 debug stderr.writefln(":: (debug) Reading JENKINS_TOKEN from file: %s", token_file); 41 } 42 catch (Exception exc){ 43 token = null; 44 } 45 } 46 47 string[] result = null; 48 auto atPosition = token.indexOf(':'); 49 if (atPosition > -1) { 50 result ~= token[0 .. atPosition]; 51 result ~= token[atPosition + 1 .. $]; 52 } 53 54 return result; 55 } 56 57 unittest { 58 auto ret = getJenkinsAuthenticationFromEnv("NON_EXISTENT"); 59 assert(ret == null); 60 61 ret = getJenkinsAuthenticationFromEnv("JENKINS_TOKEN", "user:pass"); 62 assert(ret && ret[0] == "user", "Username should be 'user'"); 63 assert(ret && ret[1] == "pass", "Password should be 'pass'"); 64 65 import core.sys.posix.stdlib; 66 import std.string: toStringz; 67 import std.conv; 68 69 string jenkinsToken = "TEST_TOKEN="; 70 putenv(cast(char*)jenkinsToken.toStringz); 71 ret = getJenkinsAuthenticationFromEnv("TEST_TOKEN"); 72 assert(ret is null, "Unable to get TEST_TOKEN"); 73 74 jenkinsToken = "TEST_TOKEN=user:pass"; 75 putenv(cast(char*)(jenkinsToken.toStringz)); 76 ret = getJenkinsAuthenticationFromEnv("TEST_TOKEN"); 77 assert(ret && ret[0] == "user", "Username should be 'user'"); 78 assert(ret && ret[1] == "pass", "Password should be 'pass'"); 79 } 80 81 // FIXME: Jenkins may return wrong output here... 82 JSONValue describeJenkinsJob(in string job_url) { 83 auto client = HTTP(); 84 auto user_pass = getJenkinsAuthenticationFromEnv("JENKINS_TOKEN"); 85 if (user_pass !is null) { 86 client.setAuthentication(user_pass[0], user_pass[1]); 87 } 88 auto content = get(job_url ~ "/api/json", client); 89 auto ret = parseJSON(content); 90 return ret; 91 } 92 93 unittest { 94 auto ret = getJenkinsAuthenticationFromEnv("JENKINS_TOKEN"); 95 assert(ret !is null, "Please set JENKINS_TOKEN in your test environment"); 96 97 import std.exception; 98 assertNotThrown(describeJenkinsJob("http://localhost:8080/job/Lauxanh-DevOps"), "jenkinsJob"); 99 } 100 101 void displayJob(JSONValue job, in string[] exitPatterns = null, in uint level = 0) { 102 if ("name" in job && "url" in job) { 103 if ( 104 exitPatterns is null 105 || exitPatterns.filter!(pat => job["url"].str.indexOf(pat) > -1).empty 106 ) 107 { 108 treeJenkinsJob(job["url"].str, exitPatterns, level + 1); 109 } 110 } 111 } 112 113 void treeJenkinsJob(in string start_url, in string[] exitPatterns = null, in uint level = 0) { 114 auto jobs = describeJenkinsJob(start_url); 115 116 if ("builds" in jobs 117 && (("color" !in jobs) || (jobs["color"].str != "disabled")) 118 && "url" in jobs 119 && "lastCompletedBuild" in jobs 120 && "lastSuccessfulBuild" in jobs 121 ) 122 { 123 auto url = jobs["url"].str; 124 auto lastCompletedBuild = (!jobs["lastCompletedBuild"].isNull() && "number" in jobs["lastCompletedBuild"]) ? jobs["lastCompletedBuild"]["number"].integer : 0; 125 auto lastSuccessfulBuild = (!jobs["lastSuccessfulBuild"].isNull() && "number" in jobs["lastSuccessfulBuild"]) ? jobs["lastSuccessfulBuild"]["number"].integer : 0; 126 if (lastCompletedBuild) { 127 auto lastBuildData = describeJenkinsJob(format("%s/%s", url, lastCompletedBuild)); 128 size_t timestamp = 0; 129 if (!lastBuildData.isNull() && "timestamp" in lastBuildData) { 130 timestamp = lastBuildData["timestamp"].integer; 131 } 132 133 auto lastStatus = (lastCompletedBuild == lastSuccessfulBuild) ? "SUCCESS" : "FAILED"; 134 import std.range : repeat; 135 writefln("%-(%s%)%s %s %s %s", "| ".repeat(level), url, lastStatus, lastCompletedBuild, timestamp); 136 } 137 } 138 if ("jobs" in jobs) { 139 auto js = jobs["jobs"].array; 140 js.each!(job => displayJob(job, exitPatterns, level)); 141 } 142 } 143 144 unittest { 145 import std.exception; 146 assertNotThrown(treeJenkinsJob("http://localhost:8080/job/Lauxanh-DevOps/", ["job/kyanh", "job/Lauxanh-PR-Review"])); 147 }