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 }