File Coverage

blib/lib/App/JenkinsCli.pm
Criterion Covered Total %
statement 30 237 12.6
branch 0 94 0.0
condition 0 20 0.0
subroutine 10 35 28.5
pod 15 15 100.0
total 55 401 13.7


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   47835 use Moo;
  1         7623  
  1         3  
10 1     1   996 use warnings;
  1         2  
  1         22  
11 1     1   4 use Carp;
  1         3  
  1         39  
12 1     1   361 use Data::Dumper qw/Dumper/;
  1         4915  
  1         49  
13 1     1   267 use English qw/ -no_match_vars /;
  1         2416  
  1         4  
14 1     1   524 use Jenkins::API;
  1         121085  
  1         33  
15 1     1   384 use Term::ANSIColor qw/colored/;
  1         7406  
  1         473  
16 1     1   268 use File::ShareDir qw/dist_dir/;
  1         4616  
  1         46  
17 1     1   496 use Path::Tiny;
  1         8232  
  1         40  
18 1     1   604 use DateTime;
  1         356863  
  1         2502  
19              
20             our $VERSION = "0.011";
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 0         if ( -f $config ) {
197 0           $config = path($config)->slurp;
198             }
199              
200 0           my $success = $jenkins->create_job($job, $config);
201              
202 0 0         print $success ? "Created $job\n" : "Error creating $job\n";
203              
204 0           return;
205             }
206              
207             sub load {
208 0     0 1   my ($self, $job, $config, @extra) = @_;
209 0           my $jenkins = $self->jenkins();
210              
211 0           print Dumper $jenkins->load_statistics();
212              
213 0           return;
214             }
215              
216             sub watch {
217 0     0 1   my ($self, @jobs) = @_;
218 0           my $jenkins = $self->jenkins();
219              
220 0 0         if ( ! defined $self->opt->regexp ) {
221 0           $self->opt->regexp(1);
222             }
223              
224 0   0       $self->opt->{sleep} ||= 30;
225 0           my $query = join '|', @jobs;
226              
227 0           while (1) {
228 0           my @out;
229 0           my $ls = $self->_ls_job($jenkins, 1);
230 0           print "\n...\n";
231              
232             $self->_action(0, $query, sub {
233 0     0     push @out, $ls->(@_);
234 0           });
235              
236 0           print "\e[2J\e[0;0H\e[K";
237 0           print "Jenkins Jobs: ", (join ', ', @jobs), "\n\n";
238 0           print sort _alpha_num @out;
239 0           sleep $self->opt->{sleep};
240             }
241              
242 0           return;
243             }
244              
245             sub enable {
246 0     0 1   my ($self, $query) = @_;
247              
248 0           my $xsl = path(dist_dir('App-JenkinsCli'), 'enable.xsl');
249 0           $self->_xslt_actions($query, $xsl);
250              
251 0           return;
252             }
253              
254             sub disable {
255 0     0 1   my ($self, $query) = @_;
256              
257 0           my $xsl = path(dist_dir('App-JenkinsCli'), 'disable.xsl');
258 0           $self->_xslt_actions($query, $xsl);
259              
260 0           return;
261             }
262              
263             sub change {
264 0     0 1   my ($self, $query, $xsl) = @_;
265              
266 0           $self->_xslt_actions($query, $xsl);
267              
268 0           return;
269             }
270              
271             sub copy {
272 0     0 1   my ($self, $old, $new) = @_;
273 0           my $jenkins = $self->jenkins();
274              
275 0 0         _error("Must provide job name to get it's configuration!\n") if !$old;
276              
277 0 0         my $config = -f $old ? path($old)->slurp : $jenkins->project_config($old);
278              
279 0           my $success = $jenkins->create_job($new, $config);
280              
281 0 0         print $success ? "Copied $new from $old\n" : "Error copying $new from $old\n";
282              
283 0           return;
284             }
285              
286             sub _xslt_actions {
287 0     0     my ($self, $query, $xsl) = @_;
288 0           require XML::LibXML;
289 0           require XML::LibXSLT;
290              
291 0           my $xslt = XML::LibXSLT->new();
292 0           my $style_doc = XML::LibXML->load_xml(location => $xsl);
293 0           my $stylesheet = $xslt->parse_stylesheet($style_doc);
294              
295 0           my $jenkins = $self->jenkins();
296              
297 0           my $data = $jenkins->_json_api([qw/api json/], { extra_params => { depth => 0 } });
298              
299 0           my %found;
300             $self->_action(0, $query, sub {
301              
302 0     0     my $config = $jenkins->project_config($_->{name});
303 0           my $dom = XML::LibXML->load_xml(string => $config);
304              
305 0           my $results = $stylesheet->transform($dom);
306 0           my $output = $stylesheet->output_as_bytes($results);
307              
308 0 0         warn "Updating $_->{name}\n" if $self->opt->{verbose};
309 0 0         if ($self->opt->{test}) {
310 0           print "$output\n";
311             }
312             else {
313 0           my $success = $jenkins->set_project_config($_->{name}, $output);
314 0 0         if (!$success) {
315 0           warn "Error in updating $_->{name}\n";
316 0           last;
317             }
318             }
319 0           });
320              
321 0           return;
322             }
323              
324             sub _action {
325 0     0     my ($self, $depth, $query, $action) = @_;
326 0           my $jenkins = $self->jenkins();
327              
328 0           my $data = eval {
329 0           $jenkins->_json_api([qw/api json/], { extra_params => { depth => $depth } });
330             };
331              
332 0 0 0       if ( ! $data || $@ ) {
333 0 0         my $err = $@ ? ": $@" : '';
334 0           confess "No data found! (can't talk to Jenkins Server? depth = $depth)$err";
335             }
336              
337 0 0         my $re = $self->opt->regexp ? qr/$query/ : qr/\A\Q$query\E\Z/;
338              
339 0           for my $job (sort _alpha_num @{ $data->{jobs} }) {
  0            
340 0 0 0       next if $query && $job->{name} !~ /$re/;
341              
342 0           local $_ = $job;
343              
344 0 0         if ( $self->opt->{recipient} ) {
345 0           my $config = $jenkins->project_config($_->{name});
346 0           require XML::Simple;
347 0           local $Data::Dumper::Sortkeys = 1;
348 0           local $Data::Dumper::Indent = 1;
349 0           my $data = XML::Simple::XMLin($config);
350 0           my $recipient = $self->opt->{recipient};
351 0 0         next if $data->{publishers}{'hudson.tasks.Mailer'}{recipients} !~ /$recipient/;
352             }
353              
354 0           $self->$action();
355             }
356              
357 0           return;
358             }
359              
360             sub _ls_job {
361 0     0     my ($self, $jenkins, $return) = @_;
362 0           my ($max, $space) = (0, 8);
363              
364             return sub {
365 0     0     my $name = $_->{name};
366 0           my ($extra_pre, $extra_post) = ('') x 2;
367              
368 0 0         if ( ! $_->{color} ) {
    0          
369 0           $_->{color} = '';
370             }
371             elsif ( $_->{color} =~ s/_anime// ) {
372 0           $extra_pre = '*';
373             }
374              
375 0 0         if ( $self->opt->{verbose} ) {
376             eval {
377             my $details = $jenkins->_json_api(
378 0           ['job', $_->{name}, qw/api json/],
379             {
380             extra_params => {
381             depth => 1,
382             tree => 'lastBuild[timestamp,displayName,builtOn,duration]'
383             }
384             }
385             );
386 0           my $duration = 'Never run';
387 0 0         if ( $details->{lastBuild}{duration} ) {
388 0           $duration = $details->{lastBuild}{duration} / 1_000;
389 0 0         if ( $duration > 2 * 60 * 60 ) {
    0          
    0          
    0          
390 0           $duration = int($duration / 60 / 60) . ' hrs';
391             }
392             elsif ( $duration >= 60 * 60 ) {
393 0           $duration = '1 hr ' . (int( ($duration - 60 * 60) / 60 )) . ' min';
394             }
395             elsif ( $duration > 2 * 60 ) {
396 0           $duration = int($duration / 60 ) . ' min';
397             }
398             elsif ( $duration >= 60 ) {
399 0           $duration = '1 min ' . ($duration - 60) . ' sec';
400             }
401             else {
402 0           $duration .= ' sec';
403             }
404             }
405              
406 0   0       $extra_post .= DateTime->from_epoch( epoch => ( $details->{lastBuild}{timestamp} || 0 ) / 1000 );
407 0 0 0       if ( $details->{lastBuild}{displayName} && $details->{lastBuild}{builtOn} ) {
408 0           $extra_post .= " ($duration / $details->{lastBuild}{displayName} / $details->{lastBuild}{builtOn})";
409             }
410             else {
411 0           $extra_post .= "Never run";
412             }
413 0           1;
414 0 0         } or do {
415 0           warn "Error getting job $_->{name}'s details: $@\n";
416             };
417 0           $name = $self->base_url . 'job/' . $name;
418             }
419              
420             # map "jenkins" colours to real colours
421 0   0       my $color = $self->colour_map->{$_->{color}} || [$_->{color}];
422              
423 0 0         if ( !$max ) {
    0          
424 0           $max = $space + length $name . " $extra_pre";
425             }
426             elsif ( length $name > $max ) {
427 0           $max = $space + length $name . " $extra_pre";
428 0 0         $space -= 2 if $space > 2;
429             }
430              
431             my $out = $self->opt->{color}
432 0 0         ? colored($color, sprintf "% -${max}s", "$name $extra_pre") . " $extra_post\n"
433             : sprintf("% -${max}s", "$name $extra_pre") . " $extra_post\n";
434              
435 0 0         if ( $self->opt->{long} ) {
436 0           $out = "$_->{color} $out";
437             }
438              
439 0 0         if ($return) {
440 0           return $out;
441             }
442 0           print $out;
443 0           };
444             }
445              
446             sub _error {
447 0     0     my ($msg) = @_;
448              
449 0           warn $msg;
450 0           exit 1;
451             }
452              
453             1;
454              
455             __END__
456              
457             =head1 NAME
458              
459             App::JenkinsCli - Command line tool for interacting with Jenkins
460              
461             =head1 VERSION
462              
463             This documentation refers to App::JenkinsCli version 0.011
464              
465             =head1 SYNOPSIS
466              
467             use App::JenkinsCli;
468              
469             # Brief but working code example(s) here showing the most common usage(s)
470             # This section will be as far as many users bother reading, so make it as
471             # educational and exemplary as possible.
472              
473              
474             =head1 DESCRIPTION
475              
476             =head1 SUBROUTINES/METHODS
477              
478             =head2 C<ls ($query)>
479              
480             =head2 C<list ($query)>
481              
482             List all jobs, optionally filtering with C<$query>
483              
484             =head2 C<start ($job)>
485              
486             Start C<$job>
487              
488             =head2 C<delete ($job)>
489              
490             Delete C<$job>
491              
492             =head2 C<status ($job)>
493              
494             Status of C<$job>
495              
496             =head2 C<enable ($job)>
497              
498             enable C<$job>
499              
500             =head2 C<disable ($job)>
501              
502             disable C<$job>
503              
504             =head2 C<conf ($job)>
505              
506             =head2 C<config ($job)>
507              
508             Show the config of C<$job>
509              
510             =head2 C<queue ()>
511              
512             Show the queue of running jobs
513              
514             =head2 C<create ($job)>
515              
516             Create a new Jenkins job
517              
518             =head2 C<load ()>
519              
520             Show the load stats for the server
521              
522             =head2 C<change ($query, $xsl)>
523              
524             Run the XSLT file (C<$xsl>) over each job matching C<$query> to generate a
525             new config which is then sent back to Jenkins.
526              
527             =head2 C<copy ( $old, $new )>
528              
529             Copy C<$old> to C<$new>
530              
531             =head2 C<watch ($job)>
532              
533             Watch jobs to track changes.
534              
535             =head1 ATTRIBUTES
536              
537             =over 4
538              
539             =item base_url
540              
541             The base URL of Jenkins
542              
543             =item api_key
544              
545             The username to access jenkins by
546              
547             =item api_pass
548              
549             The password to access jenkins by
550              
551             =item test
552              
553             Flag to not actually perform changes
554              
555             =item jenkins
556              
557             Internal L<Jenkins::API> object
558              
559             =item colours
560              
561             Mapping of Jenkins states to L<Term::ANSIColor>s
562              
563             =item opt
564              
565             User options
566              
567             =back
568              
569             =head1 DIAGNOSTICS
570              
571             =head1 CONFIGURATION AND ENVIRONMENT
572              
573             =head1 DEPENDENCIES
574              
575             =head1 INCOMPATIBILITIES
576              
577             =head1 BUGS AND LIMITATIONS
578              
579             There are no known bugs in this module.
580              
581             Please report problems to Ivan Wills (ivan.wills@gmail.com).
582              
583             Patches are welcome.
584              
585             =head1 ALSO SEE
586              
587             Inspired by https://github.com/Netflix-Skunkworks/jenkins-cli
588              
589             =head1 AUTHOR
590              
591             Ivan Wills - (ivan.wills@gmail.com)
592              
593             =head1 LICENSE AND COPYRIGHT
594              
595             Copyright (c) 2016 Ivan Wills (14 Mullion Close, Hornsby Heights, NSW Australia 2077).
596             All rights reserved.
597              
598             This module is free software; you can redistribute it and/or modify it under
599             the same terms as Perl itself. See L<perlartistic>. This program is
600             distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
601             without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
602             PARTICULAR PURPOSE.
603              
604             =cut