File Coverage

blib/lib/App/JenkinsCli.pm
Criterion Covered Total %
statement 30 225 13.3
branch 0 84 0.0
condition 0 20 0.0
subroutine 10 33 30.3
pod 14 14 100.0
total 54 376 14.3


line stmt bran cond sub pod time code
1             package App::JenkinsCli;
2              
3             # Created on: 2016-05-20 07:52:28
4             # Create by: Ivan Wills
5             # $Id$
6             # $Revision$, $HeadURL$, $Date$
7             # $Revision$, $Source$, $Date$
8              
9 1     1   82099 use Moo;
  1         12928  
  1         7  
10 1     1   1680 use warnings;
  1         4  
  1         38  
11 1     1   7 use Carp;
  1         5  
  1         65  
12 1     1   514 use Data::Dumper qw/Dumper/;
  1         9102  
  1         80  
13 1     1   572 use English qw/ -no_match_vars /;
  1         5559  
  1         7  
14 1     1   981 use Jenkins::API;
  1         156941  
  1         42  
15 1     1   404 use Term::ANSIColor qw/colored/;
  1         7484  
  1         555  
16 1     1   347 use File::ShareDir qw/dist_dir/;
  1         7203  
  1         78  
17 1     1   722 use Path::Tiny;
  1         14012  
  1         82  
18 1     1   835 use DateTime;
  1         489696  
  1         3435  
19              
20             our $VERSION = "0.010";
21              
22             has [qw/base_url api_key api_pass test/] => (
23             is => 'rw',
24             );
25             has jenkins => (
26             is => 'rw',
27             lazy => 1,
28             builder => '_jenkins',
29             );
30             has colours => (
31             is => 'rw',
32             required => 1,
33             );
34             has colour_map => (
35             is => 'rw',
36             lazy => 1,
37             default => sub {
38             my ($self) = @_;
39             return {
40             '' => ['reset'],
41             map {
42             ( $_ => [ split /\s+/, $self->colours->{$_} ] )
43             }
44             keys %{ $self->colours }
45             };
46             },
47             );
48             has opt => (
49             is => 'rw',
50             required => 1,
51             );
52              
53             sub _jenkins {
54 0     0     my ($self) = @_;
55              
56 0           return Jenkins::API->new({
57             base_url => $self->base_url,
58             api_key => $self->api_key,
59             api_pass => $self->api_pass,
60             });
61             };
62              
63             sub _alpha_num {
64 0 0   0     my $a1 = ref $a ? $a->{name} : $a;
65 0 0         my $b1 = ref $b ? $b->{name} : $b;
66 0           $a1 =~ s/(\d+)/sprintf "%05d", $1/egxms;
  0            
67 0           $b1 =~ s/(\d+)/sprintf "%05d", $1/egxms;
  0            
68 0           return $a1 cmp $b1;
69             }
70              
71 0     0 1   sub ls { shift->list(@_) }
72             sub list {
73 0     0 1   my ($self, $query) = @_;
74 0           my $jenkins = $self->jenkins();
75              
76 0 0         if ( ! defined $self->opt->regexp ) {
77 0           $self->opt->regexp(1);
78             }
79              
80 0           $self->_action(0, $query, $self->_ls_job($jenkins));
81              
82 0           return;
83             }
84              
85             sub start {
86 0     0 1   my ($self, $job, @extra) = @_;
87 0           my $jenkins = $self->jenkins();
88              
89 0 0         _error("Must start build with job name!\n") if !$job;
90              
91 0           my $result = $jenkins->_json_api(['job', $job, 'api', 'json']);
92 0 0         if ( ! $result->{buildable} ) {
93 0           warn "Job is not buildable!\n";
94 0           return 1;
95             }
96 0 0 0       if ( $result->{inQueue} && ! $self->opt->force ) {
97 0           warn $result->{queueItem}{why} . "\n";
98 0           warn "View at $result->{url}\n";
99 0           return 0;
100             }
101              
102 0           $jenkins->trigger_build($job);
103              
104 0           sleep 1;
105              
106 0           $result = $jenkins->_json_api(['job', $job, 'api', 'json']);
107 0           print "View at $result->{url}\n";
108 0 0         print $result->{queueItem}{why}, "\n" if $result->{queueItem}{why};
109              
110 0           return;
111             }
112              
113             sub delete {
114 0     0 1   my ($self, @jobs) = @_;
115              
116 0 0         _error("Job name required for deleting jobs!\n") if !@jobs;
117              
118 0           for my $job (@jobs) {
119 0           my $result = $self->jenkins->delete_project($job);
120 0 0         print $result ? "Deleted $job\n" : "Errored deleting $job\n";
121             }
122              
123 0           return;
124             }
125              
126             sub status {
127 0     0 1   my ($self, $job, @extra) = @_;
128 0           my $jenkins = $self->jenkins();
129              
130 0 0         _error("Job name required to show job status!\n") if !$job;
131              
132 0           my $result = $jenkins->_json_api(['job', $job, 'api', 'json'], { extra_params => { depth => 1 } });
133              
134 0   0       my $color = $self->colour_map->{$result->{color}} || [$result->{color}];
135 0           print colored($color, $job), "\n";
136              
137 0 0         if ($self->opt->verbose) {
138 0           for my $build (@{ $result->{builds} }) {
  0            
139 0           print "$build->{displayName}\t$build->{result}\t";
140 0 0         if ( $self->opt->verbose > 1 ) {
141 0           for my $action (@{ $build->{actions} }) {
  0            
142 0 0         if ( $action->{lastBuiltRevision} ) {
143 0           print $action->{lastBuiltRevision}{SHA1};
144             }
145             }
146             }
147 0           print "\n";
148             }
149             }
150              
151 0           return;
152             }
153              
154 0     0 1   sub conf { shift->config(@_) }
155             sub config {
156 0     0 1   my ($self, $job) = @_;
157 0           my $jenkins = $self->jenkins();
158              
159 0 0         _error("Must provide job name to get it's configuration!\n") if !$job;
160              
161             $self->_action(0, $job, sub {
162 0     0     my $config = $jenkins->project_config($_->{name});
163 0 0         if ( $self->opt->{out} ) {
164 0           path($self->opt->{out}, "$_->{name}.xml")->spew($config);
165             }
166             else {
167 0           print $config;
168             }
169 0           });
170              
171 0           return;
172             }
173              
174             sub queue {
175 0     0 1   my ($self, $job, @extra) = @_;
176 0           my $jenkins = $self->jenkins();
177              
178 0           my $queue = $jenkins->build_queue();
179              
180 0 0         if ( @{ $queue->{items} } ) {
  0            
181 0           for my $item (@{ $queue->{items} }) {
  0            
182 0           print $item;
183             }
184             }
185             else {
186 0           print "The queue is empty\n";
187             }
188              
189 0           return;
190             }
191              
192             sub create {
193 0     0 1   my ($self, $job, $config, @extra) = @_;
194 0           my $jenkins = $self->jenkins();
195              
196 0           my $success = $jenkins->create_job($job, $config);
197              
198 0 0         print $success ? "Created $job\n" : "Error creating $job\n";
199              
200 0           return;
201             }
202              
203             sub load {
204 0     0 1   my ($self, $job, $config, @extra) = @_;
205 0           my $jenkins = $self->jenkins();
206              
207 0           print Dumper $jenkins->load_statistics();
208              
209 0           return;
210             }
211              
212             sub watch {
213 0     0 1   my ($self, @jobs) = @_;
214 0           my $jenkins = $self->jenkins();
215              
216 0 0         if ( ! defined $self->opt->regexp ) {
217 0           $self->opt->regexp(1);
218             }
219              
220 0   0       $self->opt->{sleep} ||= 30;
221 0           my $query = join '|', @jobs;
222              
223 0           while (1) {
224 0           my @out;
225 0           my $ls = $self->_ls_job($jenkins, 1);
226 0           print "\n...\n";
227              
228             $self->_action(0, $query, sub {
229 0     0     push @out, $ls->(@_);
230 0           });
231              
232 0           print "\e[2J\e[0;0H\e[K";
233 0           print "Jenkins Jobs: ", (join ', ', @jobs), "\n\n";
234 0           print sort _alpha_num @out;
235 0           sleep $self->opt->{sleep};
236             }
237              
238 0           return;
239             }
240              
241             sub enable {
242 0     0 1   my ($self, $query) = @_;
243              
244 0           my $xsl = path(dist_dir('App-JenkinsCli'), 'enable.xsl');
245 0           $self->_xslt_actions($query, $xsl);
246              
247 0           return;
248             }
249              
250             sub disable {
251 0     0 1   my ($self, $query) = @_;
252              
253 0           my $xsl = path(dist_dir('App-JenkinsCli'), 'disable.xsl');
254 0           $self->_xslt_actions($query, $xsl);
255              
256 0           return;
257             }
258              
259             sub change {
260 0     0 1   my ($self, $query, $xsl) = @_;
261              
262 0           $self->_xslt_actions($query, $xsl);
263              
264 0           return;
265             }
266              
267             sub _xslt_actions {
268 0     0     my ($self, $query, $xsl) = @_;
269 0           require XML::LibXML;
270 0           require XML::LibXSLT;
271              
272 0           my $xslt = XML::LibXSLT->new();
273 0           my $style_doc = XML::LibXML->load_xml(location => $xsl);
274 0           my $stylesheet = $xslt->parse_stylesheet($style_doc);
275              
276 0           my $jenkins = $self->jenkins();
277              
278 0           my $data = $jenkins->_json_api([qw/api json/], { extra_params => { depth => 0 } });
279              
280 0           my %found;
281             $self->_action(0, $query, sub {
282              
283 0     0     my $config = $jenkins->project_config($_->{name});
284 0           my $dom = XML::LibXML->load_xml(string => $config);
285              
286 0           my $results = $stylesheet->transform($dom);
287 0           my $output = $stylesheet->output_as_bytes($results);
288              
289 0 0         warn "Updating $_->{name}\n" if $self->opt->{verbose};
290 0 0         if ($self->opt->{test}) {
291 0           print "$output\n";
292             }
293             else {
294 0           my $success = $jenkins->set_project_config($_->{name}, $output);
295 0 0         if (!$success) {
296 0           warn "Error in updating $_->{name}\n";
297 0           last;
298             }
299             }
300 0           });
301              
302 0           return;
303             }
304              
305             sub _action {
306 0     0     my ($self, $depth, $query, $action) = @_;
307 0           my $jenkins = $self->jenkins();
308              
309 0           my $data = eval {
310 0           $jenkins->_json_api([qw/api json/], { extra_params => { depth => $depth } });
311             };
312              
313 0 0 0       if ( ! $data || $@ ) {
314 0 0         my $err = $@ ? ": $@" : '';
315 0           confess "No data found! (can't talk to Jenkins Server? depth = $depth)$err";
316             }
317              
318 0 0         my $re = $self->opt->regexp ? qr/$query/ : qr/\A\Q$query\E\Z/;
319              
320 0           for my $job (sort _alpha_num @{ $data->{jobs} }) {
  0            
321 0 0 0       next if $query && $job->{name} !~ /$re/;
322              
323 0           local $_ = $job;
324              
325 0 0         if ( $self->opt->{recipient} ) {
326 0           my $config = $jenkins->project_config($_->{name});
327 0           require XML::Simple;
328 0           local $Data::Dumper::Sortkeys = 1;
329 0           local $Data::Dumper::Indent = 1;
330 0           my $data = XML::Simple::XMLin($config);
331 0           my $recipient = $self->opt->{recipient};
332 0 0         next if $data->{publishers}{'hudson.tasks.Mailer'}{recipients} !~ /$recipient/;
333             }
334              
335 0           $self->$action();
336             }
337              
338 0           return;
339             }
340              
341             sub _ls_job {
342 0     0     my ($self, $jenkins, $return) = @_;
343 0           my ($max, $space) = (0, 8);
344              
345             return sub {
346 0     0     my $name = $_->{name};
347 0           my ($extra_pre, $extra_post) = ('') x 2;
348              
349 0 0         if ( ! $_->{color} ) {
    0          
350 0           $_->{color} = '';
351             }
352             elsif ( $_->{color} =~ s/_anime// ) {
353 0           $extra_pre = '*';
354             }
355              
356 0 0         if ( $self->opt->{verbose} ) {
357             eval {
358             my $details = $jenkins->_json_api(
359 0           ['job', $_->{name}, qw/api json/],
360             {
361             extra_params => {
362             depth => 1,
363             tree => 'lastBuild[timestamp,displayName,builtOn,duration]'
364             }
365             }
366             );
367 0           my $duration = 'Never run';
368 0 0         if ( $details->{lastBuild}{duration} ) {
369 0           $duration = $details->{lastBuild}{duration} / 1_000;
370 0 0         if ( $duration > 2 * 60 * 60 ) {
    0          
    0          
    0          
371 0           $duration = int($duration / 60 / 60) . ' hrs';
372             }
373             elsif ( $duration >= 60 * 60 ) {
374 0           $duration = '1 hr ' . (int( ($duration - 60 * 60) / 60 )) . ' min';
375             }
376             elsif ( $duration > 2 * 60 ) {
377 0           $duration = int($duration / 60 ) . ' min';
378             }
379             elsif ( $duration >= 60 ) {
380 0           $duration = '1 min ' . ($duration - 60) . ' sec';
381             }
382             else {
383 0           $duration .= ' sec';
384             }
385             }
386              
387 0   0       $extra_post .= DateTime->from_epoch( epoch => ( $details->{lastBuild}{timestamp} || 0 ) / 1000 );
388 0 0 0       if ( $details->{lastBuild}{displayName} && $details->{lastBuild}{builtOn} ) {
389 0           $extra_post .= " ($duration / $details->{lastBuild}{displayName} / $details->{lastBuild}{builtOn})";
390             }
391             else {
392 0           $extra_post .= "Never run";
393             }
394 0           1;
395 0 0         } or do {
396 0           warn "Error getting job $_->{name}'s details: $@\n";
397             };
398 0           $name = $self->base_url . 'job/' . $name;
399             }
400              
401             # map "jenkins" colours to real colours
402 0   0       my $color = $self->colour_map->{$_->{color}} || [$_->{color}];
403              
404 0 0         if ( !$max ) {
    0          
405 0           $max = $space + length $name . " $extra_pre";
406             }
407             elsif ( length $name > $max ) {
408 0           $max = $space + length $name . " $extra_pre";
409 0 0         $space -= 2 if $space > 2;
410             }
411              
412 0           my $out = colored($color, sprintf "% -${max}s", "$name $extra_pre") . " $extra_post\n";
413              
414 0 0         if ( $self->opt->{long} ) {
415 0           $out = "$_->{color} $out";
416             }
417              
418 0 0         if ($return) {
419 0           return $out;
420             }
421 0           print $out;
422 0           };
423             }
424              
425             1;
426              
427             __END__
428              
429             =head1 NAME
430              
431             App::JenkinsCli - Command line tool for interacting with Jenkins
432              
433             =head1 VERSION
434              
435             This documentation refers to App::JenkinsCli version 0.010
436              
437             =head1 SYNOPSIS
438              
439             use App::JenkinsCli;
440              
441             # Brief but working code example(s) here showing the most common usage(s)
442             # This section will be as far as many users bother reading, so make it as
443             # educational and exemplary as possible.
444              
445              
446             =head1 DESCRIPTION
447              
448             =head1 SUBROUTINES/METHODS
449              
450             =head2 C<ls ($query)>
451              
452             =head2 C<list ($query)>
453              
454             List all jobs, optionally filtering with C<$query>
455              
456             =head2 C<start ($job)>
457              
458             Start C<$job>
459              
460             =head2 C<delete ($job)>
461              
462             Delete C<$job>
463              
464             =head2 C<status ($job)>
465              
466             Status of C<$job>
467              
468             =head2 C<enable ($job)>
469              
470             enable C<$job>
471              
472             =head2 C<disable ($job)>
473              
474             disable C<$job>
475              
476             =head2 C<conf ($job)>
477              
478             =head2 C<config ($job)>
479              
480             Show the config of C<$job>
481              
482             =head2 C<queue ()>
483              
484             Show the queue of running jobs
485              
486             =head2 C<create ($job)>
487              
488             Create a new Jenkins job
489              
490             =head2 C<load ()>
491              
492             Show the load stats for the server
493              
494             =head2 C<change ($query, $xsl)>
495              
496             Run the XSLT file (C<$xsl>) over each job matching C<$query> to generate a
497             new config which is then sent back to Jenkins.
498              
499             =head2 C<watch ($job)>
500              
501             Watch jobs to track changes.
502              
503             =head1 ATTRIBUTES
504              
505             =over 4
506              
507             =item base_url
508              
509             The base URL of Jenkins
510              
511             =item api_key
512              
513             The username to access jenkins by
514              
515             =item api_pass
516              
517             The password to access jenkins by
518              
519             =item test
520              
521             Flag to not actually perform changes
522              
523             =item jenkins
524              
525             Internal L<Jenkins::API> object
526              
527             =item colours
528              
529             Mapping of Jenkins states to L<Term::ANSIColor>s
530              
531             =item opt
532              
533             User options
534              
535             =back
536              
537             =head1 DIAGNOSTICS
538              
539             =head1 CONFIGURATION AND ENVIRONMENT
540              
541             =head1 DEPENDENCIES
542              
543             =head1 INCOMPATIBILITIES
544              
545             =head1 BUGS AND LIMITATIONS
546              
547             There are no known bugs in this module.
548              
549             Please report problems to Ivan Wills (ivan.wills@gmail.com).
550              
551             Patches are welcome.
552              
553             =head1 ALSO SEE
554              
555             Inspired by https://github.com/Netflix-Skunkworks/jenkins-cli
556              
557             =head1 AUTHOR
558              
559             Ivan Wills - (ivan.wills@gmail.com)
560              
561             =head1 LICENSE AND COPYRIGHT
562              
563             Copyright (c) 2016 Ivan Wills (14 Mullion Close, Hornsby Heights, NSW Australia 2077).
564             All rights reserved.
565              
566             This module is free software; you can redistribute it and/or modify it under
567             the same terms as Perl itself. See L<perlartistic>. This program is
568             distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
569             without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
570             PARTICULAR PURPOSE.
571              
572             =cut