File Coverage

blib/lib/Teamcity/Executor.pm
Criterion Covered Total %
statement 35 135 25.9
branch 0 14 0.0
condition 0 2 0.0
subroutine 12 20 60.0
pod 0 7 0.0
total 47 178 26.4


line stmt bran cond sub pod time code
1             package Teamcity::Executor;
2 3     3   101268 use 5.020;
  3         11  
3 3     3   18 use strict;
  3         7  
  3         62  
4 3     3   12 use warnings;
  3         4  
  3         104  
5              
6             our $VERSION = "0.1.1";
7              
8 3     3   1109 use Moose;
  3         1220714  
  3         22  
9 3     3   21405 use autobox::Core;
  3         43638  
  3         24  
10 3     3   3171 use HTTP::Tiny;
  3         112300  
  3         124  
11 3     3   1293 use Cpanel::JSON::XS;
  3         4838  
  3         167  
12 3     3   824 use IO::Async::Timer::Periodic;
  3         35586  
  3         103  
13              
14 3     3   22 use feature 'say';
  3         6  
  3         339  
15 3     3   24 use feature 'signatures';
  3         6  
  3         71  
16 3     3   14 no warnings 'experimental::signatures';
  3         5  
  3         1137  
17              
18             has credentials => (is => 'ro', isa => 'HashRef');
19              
20             has build_id_mapping => (is => 'ro', isa => 'HashRef');
21              
22             has http => (
23             is => 'ro', isa => 'HTTP::Tiny',
24             default => sub { HTTP::Tiny->new(timeout => 10) }
25             );
26              
27             has loop => (
28             is => 'ro', isa => 'IO::Async::Loop',
29             );
30              
31             has teamcity_builds => (
32             is => 'ro', isa => 'HashRef', default => sub { {} },
33             );
34              
35             has poll_interval => (
36             is => 'ro', isa => 'Int', default => 10,
37             );
38              
39             has teamcity_auth_url => (
40             is => 'ro', isa => 'Str', lazy => 1, default => sub ($self) {
41             my $url = $self->credentials->{url};
42             my $user = $self->credentials->{user};
43             my $pass = $self->credentials->{pass};
44              
45             my ($protocol, $address) = $url =~ m{(http[s]://)(.*)};
46              
47             return $protocol . $user . ':' . $pass . '@' . $address;
48             }
49             );
50              
51              
52 0     0 0   sub http_request($self, $method, $url, $headers = {}, $content = '') {
  0            
  0            
  0            
  0            
  0            
  0            
53              
54 0           my $desecretized_url = $url =~ s{(http[s]://)[^/]+:[^@]+@}{$1}r;
55             # say STDERR "# $method\t$desecretized_url";
56              
57 0           my $response;
58              
59 0           my $retry = 0;
60 0           while (1) {
61 0           $response = $self->http->request($method, $url, {
62             headers => $headers,
63             content => $content,
64             });
65              
66 0 0         last if $response->{status} != 599;
67 0 0         print ' [TeamCity request retry: ' if !$retry;
68 0           print '.';
69 0           sleep 1;
70 0           $retry = 1;
71             }
72 0 0         print "] " if $retry;
73              
74             # say STDERR 'done';
75              
76 0 0         if (! $response->{success} ) {
77 3     3   1227 use Data::Dumper;
  3         14015  
  3         2825  
78 0           print Dumper $response;
79 0           die "HTTP $method request to $url failed: " .
80             "$response->{status}: $response->{reason}"
81             }
82              
83 0           return $response
84             }
85              
86              
87             sub run_teamcity_build {
88 0     0 0   my ($self, $build_type_id, $properties, $build_name) = @_;
89              
90 0   0       $build_name //= 'unnamed-build';
91              
92 0           my $build_queue_url =
93             $self->teamcity_auth_url . '/httpAuth/app/rest/buildQueue';
94              
95 0           my $xml_properties = '';
96              
97 0           for my $key ($properties->keys) {
98 0           my $value = $properties->{$key};
99 0           $xml_properties .= qq{<property name="$key" value="$value" />\n};
100             }
101              
102 0           my $request_body =
103             qq{<build>
104             <buildType id="$build_type_id"/>
105             <properties>
106             $xml_properties
107             </properties>
108             </build>};
109              
110 0           my $response = $self->http_request(
111             'POST',
112             $build_queue_url,
113             {
114             'Content-Type' => 'application/xml',
115             'Accept' => 'application/json',
116             },
117             $request_body,
118             );
119              
120 0           my $json = decode_json $response->{content};
121              
122 0           my $build_id = $json->{id};
123 0           my $build_href = $json->{href};
124 0           my $f = $self->loop->new_future();
125              
126 0           $self->teamcity_builds->{$build_id} = {
127             id => $build_id,
128             href => $build_href,
129             name => $build_name,
130             future => $f,
131             };
132              
133             return $f, $build_id, $json->{webUrl}
134 0           }
135              
136 0     0 0   sub get_artifact_list($self, $build_result) {
  0            
  0            
  0            
137              
138             # get build result
139 0           my $result_url = $self->teamcity_auth_url . $build_result->{href};
140 0           my $response = $self->http_request(
141             'GET',
142             $result_url,
143             { 'Accept' => 'application/json' },
144             );
145 0           my $json = decode_json $response->{content};
146              
147             # get artifacts summary
148 0           my $artifacts_href = $json->{artifacts}{href};
149 0           my $artifacts_url = $self->teamcity_auth_url . $artifacts_href;
150 0           $response = $self->http_request(
151             'GET',
152             $artifacts_url,
153             { 'Accept' => 'application/json' },
154             );
155              
156 0           $json = decode_json $response->{content};
157              
158 0           my %artifacts;
159             # get individual artifacts URLs
160 0           for my $node ($json->{file}->elements) {
161 0           my $content_href = $node->{content}{href};
162 0           my $metadata_href = $node->{content}{href};
163 0           my $name = $node->{name};
164 0           $artifacts{$name} = {
165             name => $name,
166             content_href => $content_href,
167             metadata_href => $metadata_href,
168             };
169             }
170              
171 0           return \%artifacts
172             }
173              
174 0     0 0   sub get_artifact_content($self, $build_result, $artifact_name) {
  0            
  0            
  0            
  0            
175 0           my $artifact_list = $self->get_artifact_list($build_result);
176              
177             my $content_url = $self->teamcity_auth_url .
178 0           $artifact_list->{$artifact_name}{content_href};
179 0           my $response = $self->http_request('GET', $content_url);
180              
181             return $response->{content}
182 0           }
183              
184              
185 0     0 0   sub run($self, $build_name, $properties = {}) {
  0            
  0            
  0            
  0            
186 0           print "RUN\t$build_name(";
187 0           print join(', ', map { "$_: '$properties->{$_}'" } $properties->keys);
  0            
188 0           print ")";
189              
190             my ($f, $id, $url) = $self->run_teamcity_build(
191 0           $self->build_id_mapping->{$build_name},
192             $properties,
193             $build_name,
194             );
195              
196 0           say " [$id]\n\t$url";
197              
198 0           return $f;
199             }
200              
201 0     0 0   sub poll_teamcity_results($self) {
  0            
  0            
202 0           say 'TICK';
203 0           for my $build ($self->teamcity_builds->values) {
204 0           my $url = $self->teamcity_auth_url . $build->{href};
205 0           my $response = $self->http_request(
206             'GET',
207             $url,
208             { 'Accept' => 'application/json' },
209             );
210              
211 0           my $json = decode_json $response->{content};
212              
213 0           my $state = $json->{state};
214 0           my $status = $json->{status};
215              
216 0 0         next if $state ne 'finished';
217              
218 0           say "RESULT\t$build->{name} [$build->{id}]: $status";
219              
220 0 0         if ($status eq 'SUCCESS') {
    0          
221 0           my $href = $json->{href};
222 0           $build->{future}->done({ id => $build->{id}, href => $href });
223             }
224             elsif ($status eq 'FAILURE') {
225 0           $build->{future}->fail($json->{statusText});
226             }
227              
228 0           delete $self->teamcity_builds->{$build->{id}};
229             }
230             }
231              
232 0     0 0   sub register_polling_timer($self) {
  0            
  0            
233             my $timer = IO::Async::Timer::Periodic->new(
234             interval => $self->poll_interval,
235             on_tick => sub {
236 0     0     $self->poll_teamcity_results();
237             },
238 0           );
239              
240 0           $self->loop->add($timer);
241 0           $timer->start();
242             }
243              
244              
245             1;
246             __END__
247              
248             =encoding utf-8
249              
250             =head1 NAME
251              
252             Teamcity::Executor - Executor of TeamCity build configurations
253              
254             =head1 SYNOPSIS
255              
256             use Teamcity::Executor;
257             use IO::Async::Loop;
258              
259             my $loop = IO::Async::Loop->new;
260             my $tc = Teamcity::Executor->new(
261             credentials => {
262             url => 'https://teamcity.example.com',
263             user => 'user',
264             pass => 'password',
265             },
266             build_id_mapping => {
267             hello_world => 'playground_HelloWorld',
268             hello_name => 'playground_HelloName',
269             }
270             poll_interval => 10,
271             loop => $loop,
272             )
273              
274             $tc->register_polling_timer();
275              
276             $tc->run('hello_name', { name => 'TeamCity' })->then(
277             sub {
278             my ($build) = @_;
279             print "Build succeeded\n";
280             my $greeting = $tc->get_artifact_content($build, 'greeting.txt');
281             print "Content of greeting.txt artifact: $greeting\n";
282             },
283             sub {
284             print "Build failed\n";
285             exit 1
286             }
287             );
288              
289             $loop->run();
290              
291              
292             =head1 DESCRIPTION
293              
294             Teamcity::Executor is a module for executing Teamcity build configurations.
295             When you execute one, you'll receive a future of the build. Teamcity::Executor
296             polls TeamCity and when it finds the build has ended, it resolves the future.
297              
298             =head1 LICENSE
299              
300             Copyright (C) Avast Software
301              
302             This library is free software; you can redistribute it and/or modify
303             it under the same terms as Perl itself.
304              
305             =head1 AUTHOR
306              
307             Miroslav Tynovsky E<lt>tynovsky@avast.comE<gt>
308              
309             =cut
310