Group
Extension

App-JenkinsCli/lib/App/JenkinsCli.pm

package App::JenkinsCli;

# Created on: 2016-05-20 07:52:28
# Create by:  Ivan Wills
# $Id$
# $Revision$, $HeadURL$, $Date$
# $Revision$, $Source$, $Date$

use Moo;
use warnings;
use Carp;
use Data::Dumper qw/Dumper/;
use English qw/ -no_match_vars /;
use Jenkins::API;
use Term::ANSIColor qw/colored/;
use File::ShareDir qw/dist_dir/;
use Path::Tiny;
use DateTime;

our $VERSION = "0.013";

has [qw/base_url api_key api_pass test/] => (
    is => 'rw',
);
has jenkins => (
    is   => 'rw',
    lazy => 1,
    builder => '_jenkins',
);
has colours => (
    is       => 'rw',
    required => 1,
);
has colour_map => (
    is      => 'rw',
    lazy    => 1,
    default => sub {
        my ($self) = @_;
        return {
            '' => ['reset'],
            map {
                ( $_ => [ split /\s+/, $self->colours->{$_} ] )
            }
            keys %{ $self->colours }
        };
    },
);
has opt => (
    is       => 'rw',
    required => 1,
);

sub _jenkins {
    my ($self) = @_;

    return Jenkins::API->new({
        base_url => $self->base_url,
        api_key  => $self->api_key,
        api_pass => $self->api_pass,
    });
};

sub _alpha_num {
    my $a1 = ref $a ? $a->{name} : $a;
    my $b1 = ref $b ? $b->{name} : $b;
    $a1 =~ s/(\d+)/sprintf "%05d", $1/egxms;
    $b1 =~ s/(\d+)/sprintf "%05d", $1/egxms;
    return $a1 cmp $b1;
}

sub ls { shift->list(@_) }
sub list {
    my ($self, $query) = @_;
    my $jenkins = $self->jenkins();

    if ( ! defined $self->opt->regexp ) {
        $self->opt->regexp(1);
    }

    $self->_action(0, $query, $self->_ls_job($jenkins));

    return;
}

sub start {
    my ($self, $job, @extra) = @_;
    my $jenkins = $self->jenkins();

    _error("Must start build with job name!\n") if !$job;

    my $result = $jenkins->_json_api(['job', $job, 'api', 'json']);
    if ( ! $result->{buildable} ) {
        warn "Job is not buildable!\n";
        return 1;
    }
    if ( $result->{inQueue} && ! $self->opt->force ) {
        warn $result->{queueItem}{why} . "\n";
        warn "View at $result->{url}\n";
        return 0;
    }

    $jenkins->trigger_build($job);

    sleep 1;

    $result = $jenkins->_json_api(['job', $job, 'api', 'json']);
    print "View at $result->{url}\n";
    print $result->{queueItem}{why}, "\n" if $result->{queueItem}{why};

    return;
}

sub delete {
    my ($self, @jobs) = @_;

    _error("Job name required for deleting jobs!\n") if !@jobs;

    for my $job (@jobs) {
        my $result = $self->jenkins->delete_project($job);
        print $result ? "Deleted $job\n" : "Errored deleting $job\n";
    }

    return;
}

sub status {
    my ($self, $job, @extra) = @_;
    my $jenkins = $self->jenkins();

    _error("Job name required to show job status!\n") if !$job;

    my $result = $jenkins->_json_api(['job', $job, 'api', 'json'], { extra_params => { depth => 1 } });

    my $color = $self->colour_map->{$result->{color}} || [$result->{color}];
    print colored($color, $job), "\n";

    if ($self->opt->verbose) {
        for my $build (@{ $result->{builds} }) {
            print "$build->{displayName}\t$build->{result}\t";
            if ( $self->opt->verbose > 1 ) {
                for my $action (@{ $build->{actions} }) {
                    if ( $action->{lastBuiltRevision} ) {
                        print $action->{lastBuiltRevision}{SHA1};
                    }
                }
            }
            print "\n";
        }
    }

    return;
}

sub conf { shift->config(@_) }
sub config {
    my ($self, $job) = @_;
    my $jenkins = $self->jenkins();

    _error("Must provide job name to get it's configuration!\n") if !$job;

    $self->_action(0, $job, sub {
        my $config = $jenkins->project_config($_->{name});
        if ( $self->opt->{out} ) {
            path($self->opt->{out}, "$_->{name}.xml")->spew($config);
        }
        else {
            print $config;
        }
    });

    return;
}

sub queue {
    my ($self, $job, @extra) = @_;
    my $jenkins = $self->jenkins();

    my $queue = $jenkins->build_queue();

    if ( @{ $queue->{items} } ) {
        for my $item (@{ $queue->{items} }) {
            print $item;
        }
    }
    else {
        print "The queue is empty\n";
    }

    return;
}

sub create {
    my ($self, $job, $config, @extra) = @_;
    my $jenkins = $self->jenkins();

    if ( -f $config ) {
        $config = path($config)->slurp;
    }

    my $success = $jenkins->create_job($job, $config);

    print $success ? "Created $job\n" : "Error creating $job\n";

    return;
}

sub load {
    my ($self, $job, $config, @extra) = @_;
    my $jenkins = $self->jenkins();

    print Dumper $jenkins->load_statistics();

    return;
}

sub watch {
    my ($self, @jobs) = @_;
    my $jenkins = $self->jenkins();

    if ( ! defined $self->opt->regexp ) {
        $self->opt->regexp(1);
    }

    $self->opt->{sleep} ||= 30;
    my $query = join '|', @jobs;

    while (1) {
        my @out;
        my $ls = $self->_ls_job($jenkins, 1);
        print "\n...\n";

        $self->_action(0, $query, sub {
            push @out, $ls->(@_);
        });

        print "\e[2J\e[0;0H\e[K";
        print "Jenkins Jobs: ", (join ', ', @jobs), "\n\n";
        print sort _alpha_num @out;
        sleep $self->opt->{sleep};
    }

    return;
}

sub enable {
    my ($self, $query) = @_;

    my $xsl = path(dist_dir('App-JenkinsCli'), 'enable.xsl');
    $self->_xslt_actions($query, $xsl);

    return;
}

sub disable {
    my ($self, $query) = @_;

    my $xsl = path(dist_dir('App-JenkinsCli'), 'disable.xsl');
    $self->_xslt_actions($query, $xsl);

    return;
}

sub change {
    my ($self, $query, $xsl) = @_;

    $self->_xslt_actions($query, $xsl);

    return;
}

sub copy {
    my ($self, $old, $new) = @_;
    my $jenkins = $self->jenkins();

    _error("Must provide job name to get it's configuration!\n") if !$old;

    my $config = -f $old ? path($old)->slurp : $jenkins->project_config($old);

    my $success = $jenkins->create_job($new, $config);

    print $success ? "Copied $new from $old\n" : "Error copying $new from $old\n";

    return;
}

sub _xslt_actions {
    my ($self, $query, $xsl) = @_;
    require XML::LibXML;
    require XML::LibXSLT;

    my $xslt = XML::LibXSLT->new();
    my $style_doc = XML::LibXML->load_xml(location => $xsl);
    my $stylesheet = $xslt->parse_stylesheet($style_doc);

    my $jenkins = $self->jenkins();

    my $data = $jenkins->_json_api([qw/api json/], { extra_params => { depth => 0 } });

    my %found;
    $self->_action(0, $query, sub {

        my $config = $jenkins->project_config($_->{name});
        my $dom = XML::LibXML->load_xml(string => $config);

        my $results = $stylesheet->transform($dom);
        my $output  = $stylesheet->output_as_bytes($results);

        warn "Updating $_->{name}\n" if $self->opt->{verbose};
        if ($self->opt->{test}) {
            print "$output\n";
        }
        else {
            my $success = $jenkins->set_project_config($_->{name}, $output);
            if (!$success) {
                warn "Error in updating $_->{name}\n";
                last;
            }
        }
    });

    return;
}

sub _action {
    my ($self, $depth, $query, $action) = @_;
    my $jenkins = $self->jenkins();

    my $data = eval {
        $jenkins->_json_api([qw/api json/], { extra_params => { depth => $depth } });
    };

    if ( ! $data || $@ ) {
        my $err = $@ ? ": $@" : '';
        confess "No data found! (can't talk to Jenkins Server? depth = $depth)$err";
    }

    my $re = $self->opt->regexp ? qr/$query/ : qr/\A\Q$query\E\Z/;

    for my $job (sort _alpha_num @{ $data->{jobs} }) {
        next if $query && $job->{name} !~ /$re/;

        local $_ = $job;

        if ( $self->opt->{recipient} ) {
            my $config = $jenkins->project_config($_->{name});
            require XML::Simple;
            local $Data::Dumper::Sortkeys = 1;
            local $Data::Dumper::Indent = 1;
            my $data = XML::Simple::XMLin($config);
            my $recipient = $self->opt->{recipient};
            next if $data->{publishers}{'hudson.tasks.Mailer'}{recipients} !~ /$recipient/;
        }

        $self->$action();
    }

    return;
}

sub _ls_job {
    my ($self, $jenkins, $return) = @_;
    my ($max, $space) = (0, 8);

    return sub {
        my $name = $_->{name};
        my ($extra_pre, $extra_post) = ('') x 2;

        if ( ! $_->{color} ) {
            $_->{color} = '';
        }
        elsif ( $_->{color} =~ s/_anime// ) {
            $extra_pre = '*';
        }

        if ( $self->opt->{verbose} ) {
            eval {
                my $details = $jenkins->_json_api(
                    ['job', $_->{name}, qw/api json/],
                    {
                        extra_params => {
                            depth => 1,
                            tree => 'lastBuild[timestamp,displayName,builtOn,duration]'
                        }
                    }
                );
                my $duration = 'Never run';
                if ( $details->{lastBuild}{duration} ) {
                    $duration = $details->{lastBuild}{duration} / 1_000;
                    if ( $duration > 2 * 60 * 60 ) {
                        $duration = int($duration / 60 / 60) . ' hrs';
                    }
                    elsif ( $duration >= 60 * 60 ) {
                        $duration = '1 hr ' . (int( ($duration - 60 * 60) / 60 )) . ' min';
                    }
                    elsif ( $duration > 2 * 60 ) {
                        $duration = int($duration / 60 ) . ' min';
                    }
                    elsif ( $duration >= 60 ) {
                        $duration = '1 min ' . ($duration - 60) . ' sec';
                    }
                    else {
                        $duration .= ' sec';
                    }
                }

                $extra_post .= DateTime->from_epoch( epoch => ( $details->{lastBuild}{timestamp} || 0 ) / 1000 );
                if ( $details->{lastBuild}{displayName} && $details->{lastBuild}{builtOn} ) {
                    $extra_post .= " ($duration / $details->{lastBuild}{displayName} / $details->{lastBuild}{builtOn})";
                }
                else {
                    $extra_post .= " Never run";
                }
                1;
            } or do {
                warn "Error getting job $_->{name}'s details: $@\n";
            };
            $name = $self->base_url . 'job/' . $name;
        }

        # map "jenkins" colours to real colours
        my $color = $self->colour_map->{$_->{color}} || [$_->{color}];

        if ( !$max ) {
            $max = $space + length $name . " $extra_pre";
        }
        elsif ( length $name > $max ) {
            $max = $space + length $name . " $extra_pre";
            $space -= 2 if $space > 2;
        }

        my $out = $self->opt->{color}
            ? colored($color, sprintf "% -${max}s", "$name $extra_pre") . " $extra_post\n"
            : sprintf("% -${max}s", "$name $extra_pre") . " $extra_post\n";

        if ( $self->opt->{long} ) {
            $out = "$_->{color} $out";
        }

        if ($return) {
            return $out;
        }
        print $out;
    };
}

sub _error {
    my ($msg) = @_;

    warn $msg;
    exit 1;
}

1;

__END__

=head1 NAME

App::JenkinsCli - Command line tool for interacting with Jenkins

=head1 VERSION

This documentation refers to App::JenkinsCli version 0.013

=head1 SYNOPSIS

   use App::JenkinsCli;

   # Brief but working code example(s) here showing the most common usage(s)
   # This section will be as far as many users bother reading, so make it as
   # educational and exemplary as possible.


=head1 DESCRIPTION

=head1 SUBROUTINES/METHODS

=head2 C<ls ($query)>

Alias for list

=head2 C<list ($query)>

List all jobs, optionally filtering with C<$query>

=head2 C<start ($job)>

Start C<$job>

=head2 C<delete ($job)>

Delete C<$job>

=head2 C<status ($job)>

Status of C<$job>

=head2 C<enable ($job)>

enable C<$job>

=head2 C<disable ($job)>

disable C<$job>

=head2 C<conf ($job)>

=head2 C<config ($job)>

Show the config of C<$job>

=head2 C<queue ()>

Show the queue of running jobs

=head2 C<create ($job)>

Create a new Jenkins job

=head2 C<load ()>

Show the load stats for the server

=head2 C<change ($query, $xsl)>

Run the XSLT file (C<$xsl>) over each job matching C<$query> to generate a
new config which is then sent back to Jenkins.

=head2 C<copy ( $old, $new )>

Copy C<$old> to C<$new>

=head2 C<watch ($job)>

Watch jobs to track changes.

=head1 ATTRIBUTES

=over 4

=item base_url

The base URL of Jenkins

=item api_key

The username to access jenkins by

=item api_pass

The password to access jenkins by

=item test

Flag to not actually perform changes

=item jenkins

Internal L<Jenkins::API> object

=item colours

Mapping of Jenkins states to L<Term::ANSIColor>s

=item opt

User options

=back

=head1 DIAGNOSTICS

=head1 CONFIGURATION AND ENVIRONMENT

=head1 DEPENDENCIES

=head1 INCOMPATIBILITIES

=head1 BUGS AND LIMITATIONS

There are no known bugs in this module.

Please report problems to Ivan Wills (ivan.wills@gmail.com).

Patches are welcome.

=head1 ALSO SEE

Inspired by https://github.com/Netflix-Skunkworks/jenkins-cli

=head1 AUTHOR

Ivan Wills - (ivan.wills@gmail.com)

=head1 LICENSE AND COPYRIGHT

Copyright (c) 2016 Ivan Wills (14 Mullion Close, Hornsby Heights, NSW Australia 2077).
All rights reserved.

This module is free software; you can redistribute it and/or modify it under
the same terms as Perl itself. See L<perlartistic>.  This program is
distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.