File Coverage

blib/lib/Test/Consul.pm
Criterion Covered Total %
statement 57 182 31.3
branch 4 72 5.5
condition 2 21 9.5
subroutine 18 36 50.0
pod 9 10 90.0
total 90 321 28.0


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