File Coverage

blib/lib/Test/Consul.pm
Criterion Covered Total %
statement 57 178 32.0
branch 4 66 6.0
condition 2 18 11.1
subroutine 18 36 50.0
pod 9 10 90.0
total 90 308 29.2


line stmt bran cond sub pod time code
1             package Test::Consul;
2             $Test::Consul::VERSION = '0.016';
3             # ABSTRACT: Run a consul server for testing
4              
5 5     5   2775 use 5.010;
  5         40  
6 5     5   2326 use namespace::autoclean;
  5         90259  
  5         17  
7              
8 5     5   2732 use File::Which qw(which);
  5         5078  
  5         282  
9 5     5   1881 use JSON::MaybeXS qw(JSON encode_json);
  5         38405  
  5         335  
10 5     5   4141 use Path::Tiny;
  5         56252  
  5         275  
11 5     5   2617 use POSIX qw(WNOHANG);
  5         31625  
  5         24  
12 5     5   7115 use Carp qw(croak);
  5         10  
  5         228  
13 5     5   3638 use HTTP::Tiny v0.014;
  5         209334  
  5         268  
14 5     5   2456 use Net::EmptyPort qw(check_port);
  5         20112  
  5         321  
15 5     5   3651 use File::Temp qw(tempfile);
  5         53782  
  5         316  
16 5     5   36 use Scalar::Util qw(blessed);
  5         12  
  5         211  
17 5     5   2582 use Data::Random qw(rand_words);
  5         64679  
  5         326  
18              
19 5     5   2773 use Moo;
  5         39932  
  5         24  
20 5     5   10510 use Types::Standard qw(Bool Enum Undef);
  5         369977  
  5         51  
21 5     5   7775 use Types::Common::Numeric qw(PositiveInt PositiveOrZeroInt);
  5         62435  
  5         47  
22 5     5   5551 use Types::Common::String qw(NonEmptySimpleStr);
  5         215576  
  5         63  
23              
24             my $start_port = 49152;
25             my $current_port = $start_port;
26             my $end_port = 65535;
27              
28             sub _unique_empty_port {
29 0     0   0 my ($udp_too) = @_;
30              
31 0         0 my $port = 0;
32 0         0 while ($port == 0) {
33 0         0 $current_port ++;
34 0 0       0 $current_port = $start_port if $current_port > $end_port;
35 0 0       0 next if check_port($current_port, 'tcp');
36 0 0 0     0 next if $udp_too and check_port($current_port, 'udp');
37 0         0 $port = $current_port;
38             }
39              
40             # Make sure we return a scalar with just numeric data so it gets
41             # JSON encoded without quotes.
42 0         0 return $port;
43             }
44              
45             sub _generate_name {
46 0     0   0 my ($wordcount) = @_;
47 0         0 join '-', map { lc } rand_words(size => $wordcount, shuffle => 1);
  0         0  
48             }
49              
50             has _pid => (
51             is => 'rw',
52             predicate => '_has_pid',
53             clearer => '_clear_pid',
54             );
55              
56             has port => (
57             is => 'lazy',
58             isa => PositiveInt,
59             );
60             sub _build_port {
61 0     0   0 return _unique_empty_port();
62             }
63              
64             has serf_lan_port => (
65             is => 'lazy',
66             isa => PositiveInt,
67             );
68             sub _build_serf_lan_port {
69 0     0   0 return _unique_empty_port(1);
70             }
71              
72             has serf_wan_port => (
73             is => 'lazy',
74             isa => PositiveInt,
75             );
76             sub _build_serf_wan_port {
77 0     0   0 return _unique_empty_port(1);
78             }
79              
80             has server_port => (
81             is => 'lazy',
82             isa => PositiveInt,
83             );
84             sub _build_server_port {
85 0     0   0 return _unique_empty_port();
86             }
87              
88             has node_name => (
89             is => 'lazy',
90             isa => NonEmptySimpleStr,
91             );
92             sub _build_node_name {
93 0     0   0 _generate_name(2)
94             }
95              
96             has datacenter => (
97             is => 'lazy',
98             isa => NonEmptySimpleStr,
99             );
100             sub _build_datacenter {
101 0     0   0 _generate_name(1)
102             }
103              
104             has enable_acls => (
105             is => 'ro',
106             isa => Bool,
107             );
108              
109             has acl_default_policy => (
110             is => 'ro',
111             isa => Enum[qw(allow deny)],
112             default => 'allow',
113             );
114              
115             has acl_master_token => (
116             is => 'ro',
117             isa => NonEmptySimpleStr,
118             default => '01234567-89AB-CDEF-GHIJ-KLMNOPQRSTUV',
119             );
120              
121             has enable_remote_exec => (
122             is => 'ro',
123             isa => Bool,
124             );
125              
126             has bin => (
127             is => 'lazy',
128             isa => NonEmptySimpleStr | Undef,
129             );
130             sub _build_bin {
131 0     0   0 my ($self) = @_;
132 0         0 return $self->found_bin;
133             }
134              
135             has datadir => (
136             is => 'ro',
137             isa => NonEmptySimpleStr,
138             predicate => 1,
139             );
140              
141             has version => (
142             is => 'lazy',
143             isa => PositiveOrZeroInt,
144             default => sub {
145             my ($self) = @_;
146             return $self->found_version;
147             },
148             );
149              
150             sub running {
151 0     0 1 0 my ($self) = @_;
152 0         0 return !!$self->_has_pid();
153             }
154              
155             sub start {
156 0     0 1 0 my $self = shift;
157 0         0 my $is_class_method = 0;
158              
159 0 0       0 if (!blessed $self) {
160 0         0 $self = $self->new(@_);
161 0         0 $is_class_method = 1;
162             }
163              
164 0         0 my $bin = $self->bin();
165              
166             # Make sure we have at least Consul 0.6.1 which supports the -dev option.
167 0 0       0 unless ($self->version >= 6_001) {
168 0         0 croak "consul not version 0.6.1 or newer";
169             }
170              
171 0         0 my @opts;
172              
173 0         0 my %config = (
174             node_name => $self->node_name(),
175             datacenter => $self->datacenter(),
176             bind_addr => '127.0.0.1',
177             ports => {
178             dns => -1,
179             http => $self->port() + 0,
180             https => -1,
181             serf_lan => $self->serf_lan_port() + 0,
182             serf_wan => $self->serf_wan_port() + 0,
183             server => $self->server_port() + 0,
184             },
185             );
186              
187             # Version 0.7.0 reduced default performance behaviors in a way
188             # that makese these tests slower to startup. Override this and
189             # make leadership election happen ASAP.
190 0 0       0 if ($self->version >= 7_000) {
191 0         0 $config{performance} = { raft_multiplier => 1 };
192             }
193              
194             # gRPC health checks were added 1.0.5, and in dev mode are enabled and bind
195             # to port 8502, which then clashes if you want to run up a second
196             # Test::Consul. Just disable it.
197 0 0       0 if ($self->version >= 1_000_005) {
198 0         0 $config{ports}{grpc} = -1;
199             }
200              
201 0 0       0 if ($self->enable_acls()) {
202 0 0       0 if ($self->version >= 1_004_000) {
203 0         0 croak "ACLs not supported with Consul >= 1.4.0"
204             }
205              
206 0         0 $config{acl_master_token} = $self->acl_master_token();
207 0         0 $config{acl_default_policy} = $self->acl_default_policy();
208 0         0 $config{acl_datacenter} = $self->datacenter();
209 0         0 $config{acl_token} = $self->acl_master_token();
210             }
211              
212 0 0       0 if (defined $self->enable_remote_exec) {
213 0 0       0 $config{disable_remote_exec} = $self->enable_remote_exec ? JSON->false : JSON->true;
214             }
215              
216 0         0 my $configpath;
217 0 0       0 if ($self->has_datadir()) {
218 0         0 $config{data_dir} = $self->datadir();
219 0         0 $config{bootstrap} = \1;
220 0         0 $config{server} = \1;
221              
222 0         0 my $datapath = path($self->datadir());
223 0         0 $datapath->remove_tree;
224 0         0 $datapath->mkpath;
225              
226 0         0 $configpath = $datapath->child("consul.json");
227             }
228             else {
229 0         0 push @opts, '-dev';
230 0         0 $configpath = path((tempfile(SUFFIX => '.json'))[1]);
231             }
232              
233 0         0 $configpath->spew(encode_json(\%config));
234 0         0 push @opts, '-config-file', "$configpath";
235              
236 0         0 my $pid = fork();
237 0 0       0 unless (defined $pid) {
238 0         0 croak "fork failed: $!";
239             }
240 0 0       0 unless ($pid) {
241 0         0 exec $bin, "agent", @opts;
242             }
243              
244 0         0 my $http = HTTP::Tiny->new(timeout => 10);
245 0         0 my $now = time;
246 0         0 my $res;
247 0         0 my $port = $self->port();
248 0         0 while (time < $now+30) {
249 0         0 $res = $http->get("http://127.0.0.1:$port/v1/status/leader");
250 0 0 0     0 last if $res->{success} && $res->{content} =~ m/^"[0-9\.]+:[0-9]+"$/;
251 0         0 sleep 1;
252             }
253 0 0       0 unless ($res->{success}) {
254 0         0 kill 'KILL', $pid;
255 0         0 croak "consul API test failed: $res->{status} $res->{reason}";
256             }
257              
258 0 0       0 unlink $configpath if !$self->has_datadir();
259              
260 0         0 $self->_pid($pid);
261              
262 0 0       0 return $self if $is_class_method;
263 0         0 return;
264             }
265              
266             sub stop {
267 0     0 1 0 my ($self) = @_;
268 0 0       0 return unless $self->_has_pid();
269 0         0 my $pid = $self->_pid();
270 0         0 $self->_clear_pid();
271 0         0 kill 'TERM', $pid;
272 0         0 my $now = time;
273 0         0 while (time < $now+2) {
274 0 0       0 return if waitpid($pid, WNOHANG) > 0;
275             }
276 0         0 kill 'KILL', $pid;
277 0         0 return;
278             }
279              
280             sub end {
281 0     0 0 0 goto \&stop;
282             }
283              
284             sub DESTROY {
285 0     0   0 goto \&stop;
286             }
287              
288             sub join {
289 0     0 1 0 my ($self, $other) = @_;
290              
291 0         0 my $http = HTTP::Tiny->new(timeout => 10);
292 0         0 my $port = $self->port;
293 0         0 my $other_lan_port = $other->serf_lan_port;
294              
295 0         0 my $res = $http->put("http://127.0.0.1:$port/v1/agent/join/127.0.0.1:$other_lan_port");
296 0 0       0 unless ($res->{success}) {
297 0         0 croak "join failed: $res->{status} $res->{reason}"
298             }
299             }
300              
301             sub wan_join {
302 0     0 1 0 my ($self, $other) = @_;
303              
304 0         0 my $http = HTTP::Tiny->new(timeout => 10);
305 0         0 my $port = $self->port;
306 0         0 my $other_wan_port = $other->serf_wan_port;
307              
308 0         0 my $res = $http->put("http://127.0.0.1:$port/v1/agent/join/127.0.0.1:$other_wan_port?wan=1");
309 0 0       0 unless ($res->{success}) {
310 0         0 croak "WAN join failed: $res->{status} $res->{reason}"
311             }
312             }
313              
314             sub found_bin {
315 4     4 1 10 state ($bin, $bin_searched_for);
316 4 50       14 return $bin if $bin_searched_for;
317 4   33     33 my $binpath = $ENV{CONSUL_BIN} || which "consul";
318 4 50 33     1047 $bin = $binpath if defined($binpath) && -x $binpath;
319 4         9 $bin_searched_for = 1;
320 4         15 return $bin;
321             }
322              
323             sub skip_all_if_no_bin {
324 4     4 1 439 my ($class) = @_;
325              
326 4 50       49 croak 'The skip_all_if_no_bin method may only be used if the plan ' .
327             'function is callable on the main package (which Test::More ' .
328             'and Test2::Tools::Basic provide)'
329             if !main->can('plan');
330              
331 4 50       17 return if defined $class->found_bin();
332              
333 4         19 main::plan(skip_all => 'The Consul binary must be available to run this test.');
334             }
335              
336             sub found_version {
337 0     0 1   state ($version);
338 0 0         return $version if defined $version;
339 0           my $bin = found_bin();
340 0           ($version) = qx{$bin version};
341 0 0 0       if ($version and $version =~ m{v(\d+)\.(\d+)\.(\d+)}) {
342 0           $version = sprintf('%03d%03d%03d', $1, $2, $3);
343             }
344             else {
345 0           $version = 0;
346             }
347             }
348              
349             sub skip_all_unless_version {
350 0     0 1   my ($class, $minver, $maxver) = @_;
351              
352 0 0         croak 'usage: Test::Consul->skip_all_unless_version($minver, [$maxver])'
353             unless defined $minver;
354              
355 0 0         croak 'The skip_all_unless_version method may only be used if the plan ' .
356             'function is callable on the main package (which Test::More ' .
357             'and Test2::Tools::Basic provide)'
358             if !main->can('plan');
359              
360 0           $class->skip_all_if_no_bin;
361              
362 0           my $version = $class->found_version;
363              
364 0 0         if (defined $maxver) {
365 0 0 0       return if $minver <= $version && $maxver > $version;
366 0           main::plan(skip_all => "Consul must be between version $minver and $maxver to run this test.");
367             }
368             else {
369 0 0         return if $minver <= $version;
370 0           main::plan(skip_all => "Consul must be version $minver or higher to run this test.");
371             }
372             }
373              
374             1;
375              
376             =pod
377              
378             =encoding UTF-8
379              
380             =for markdown [![Build Status](https://secure.travis-ci.org/robn/Test-Consul.png)](http://travis-ci.org/robn/Test-Consul)
381              
382             =head1 NAME
383              
384             Test::Consul - Run a Consul server for testing
385              
386             =head1 SYNOPSIS
387              
388             use Test::Consul;
389            
390             # succeeds or dies
391             my $tc = Test::Consul->start;
392            
393             my $consul_baseurl = "http://127.0.0.1:".$tc->port;
394            
395             # do things with Consul here
396            
397             # kill test server (or let $tc fall out of scope, destructor will clean up)
398             $tc->end;
399              
400             =head1 DESCRIPTION
401              
402             This module starts and stops a standalone Consul instance. It's designed to be
403             used to help test Consul-aware Perl programs.
404              
405             It's assumed that you have Consul 0.6.4 installed somewhere.
406              
407             =head1 ARGUMENTS
408              
409             =head2 port
410              
411             The TCP port for HTTP API endpoint. Consul's default is C<8500>, but
412             this defaults to a random unused port.
413              
414             =head2 serf_lan_port
415              
416             The TCP and UDP port for the Serf LAN. Consul's default is C<8301>, but
417             this defaults to a random unused port.
418              
419             =head2 serf_wan_port
420              
421             The TCP and UDP port for the Serf WAN. Consul's default is C<8302>, but
422             this defaults to a random unused port.
423              
424             =head2 server_port
425              
426             The TCP port for the RPC Server address. Consul's default is C<8300>, but
427             this defaults to a random unused port.
428              
429             =head2 node_name
430              
431             The name of this node. If not provided, one will be generated.
432              
433             =head2 datacenter
434              
435             The name of the datacenter. If not provided, one will be generated.
436              
437             =head2 enable_acls
438              
439             Set this to true to enable ACLs. Note that Consul ACLs changed substantially in
440             Consul 1.4, and L has not yet been updated to support them. If
441             you try to enable them with Consul 1.4+, L will croak. See
442             L for more info.
443              
444             =head2 acl_default_policy
445              
446             Set this to either C or C. The default is C.
447             See L for more
448             information.
449              
450             =head2 acl_master_token
451              
452             If L is true then this token will be used as the master
453             token. By default this will be C<01234567-89AB-CDEF-GHIJ-KLMNOPQRSTUV>.
454              
455             =head2 enable_acls
456              
457             Set this to true to enable remote execution (off by default since Consul 0.8.0)
458              
459             =head2 bin
460              
461             Location of the C binary. If not provided then the binary will
462             be retrieved from L.
463              
464             =head2 datadir
465              
466             Directory for Consul's data store. If not provided, the C<-dev> option is used
467             and no datadir is used.
468              
469             =head1 ATTRIBUTES
470              
471             =head2 running
472              
473             Returns C if L has been called and L has not been called.
474              
475             =head1 METHODS
476              
477             =head2 start
478              
479             # As an object method:
480             my $tc = Test::Consul->new(%args);
481             $tc->start();
482            
483             # As a class method:
484             my $tc = Test::Consul->start(%args);
485              
486             Starts a Consul instance. This method can take a moment to run, because it
487             waits until Consul's HTTP endpoint is available before returning. If it fails
488             for any reason an exception is thrown. In this way you can be sure that Consul
489             is ready for service if this method returns successfully.
490              
491             =head2 stop
492              
493             $tc->stop();
494              
495             Kill the Consul instance. Graceful shutdown is attempted first, and if it
496             doesn't die within a couple of seconds, the process is killed.
497              
498             This method is also called if the instance of this class falls out of scope.
499              
500             =head2 join
501              
502             my $tc1 = Test::Consul->start;
503             my $tc2 = Test::Consul->start(datacenter => $tc1);
504             $tc1->wan_join($tc2);
505              
506             Perform a join to another L instance. Use this to test Consul applications that operate across nodes.
507              
508             =head2 wan_join
509              
510             my $tc1 = Test::Consul->start;
511             my $tc2 = Test::Consul->start;
512             $tc1->wan_join($tc2);
513              
514             Perform a WAN join to another L instance. Use this to test Consul
515             applications that operate across datacenters.
516              
517             =head1 CLASS METHODS
518              
519             See also L which acts as both a class and instance method.
520              
521             =head2 found_bin
522              
523             Return the value of the C env var, if set, or uses L
524             to search the system for an installed binary. Returns C if no consul
525             binary could be found.
526              
527             =head2 skip_all_if_no_bin
528              
529             Test::Consul->skip_all_if_no_bin;
530              
531             This class method issues a C on the main package if the
532             consul binary could not be found (L returns false).
533              
534             =head2 found_version
535              
536             Return the version of the consul binary, by running the binary return by
537             L with the C argument. Returns 0 if the version can't be
538             determined.
539              
540             =head2 skip_all_unless_version
541              
542             Test::Consul->skip_all_unless_version($minver, [$maxver]);
543              
544             This class method issues a C on the main package if the consul binary
545             is not between C<$minver> and C<$maxvar> (exclusive).
546              
547             =head1 SEE ALSO
548              
549             =over 4
550              
551             =item *
552              
553             L - Consul client library. Uses L in its test suite.
554              
555             =back
556              
557             =head1 SUPPORT
558              
559             =head2 Bugs / Feature Requests
560              
561             Please report any bugs or feature requests through the issue tracker
562             at L.
563             You will be notified automatically of any progress on your issue.
564              
565             =head2 Source Code
566              
567             This is open source software. The code repository is available for
568             public review and contribution under the terms of the license.
569              
570             L
571              
572             git clone https://github.com/robn/Test-Consul.git
573              
574             =head1 AUTHORS
575              
576             =over 4
577              
578             =item *
579              
580             Rob N ★
581              
582             =back
583              
584             =head1 CONTRIBUTORS
585              
586             =over 4
587              
588             =item *
589              
590             Aran Deltac
591              
592             =back
593              
594             =head1 COPYRIGHT AND LICENSE
595              
596             This software is copyright (c) 2015 by Rob N ★ and was supported by FastMail
597             Pty Ltd.
598              
599             This is free software; you can redistribute it and/or modify it under
600             the same terms as the Perl 5 programming language system itself.
601              
602             =cut