File Coverage

blib/lib/App/Puppet/Environment/Updater.pm
Criterion Covered Total %
statement 85 92 92.3
branch 4 6 66.6
condition n/a
subroutine 22 24 91.6
pod 8 8 100.0
total 119 130 91.5


line stmt bran cond sub pod time code
1             $App::Puppet::Environment::Updater::VERSION = '0.001002';
2             # ABSTRACT: Update a Puppet environment in a Git branch
3              
4             use Moose;
5 1     1   129141 use MooseX::FollowPBP;
  1         466286  
  1         6  
6 1     1   8698  
  1         13514  
  1         4  
7             with 'MooseX::Getopt';
8              
9             use Carp;
10 1     1   8394 use Git::Wrapper;
  1         2  
  1         59  
11 1     1   530 use File::pushd;
  1         34515  
  1         42  
12 1     1   441 use Path::Class::Dir;
  1         1179  
  1         58  
13 1     1   540 use MooseX::Types::Path::Class;
  1         17180  
  1         30  
14 1     1   443 use Log::Dispatchouli;
  1         412547  
  1         12  
15 1     1   1732 use List::MoreUtils qw(any);
  1         225678  
  1         50  
16 1     1   585 use Term::ANSIColor qw(:constants);
  1         13125  
  1         7  
17 1     1   1746 use Try::Tiny;
  1         10295  
  1         1008  
18 1     1   9 use namespace::autoclean;
  1         2  
  1         64  
19 1     1   8  
  1         2  
  1         18  
20              
21             has 'from' => (
22             is => 'ro',
23             isa => 'Str',
24             required => 1,
25             documentation => 'Environment/branch which should be merged',
26             );
27              
28             has 'environment' => (
29             is => 'ro',
30             isa => 'Str',
31             required => 1,
32             documentation => 'Environment/branch which should be updated',
33             );
34              
35             has 'remote' => (
36             is => 'ro',
37             isa => 'Str',
38             documentation => 'Git remote to fetch latest changes from, defaults to "origin"',
39             default => 'origin',
40             );
41              
42             has 'workdir' => (
43             is => 'ro',
44             isa => 'Path::Class::Dir',
45             coerce => 1,
46             documentation => 'Directory to work in, should be the directory with the environment that should be updated',
47             default => sub {
48             return Path::Class::Dir->new('.')->absolute();
49             },
50             );
51              
52             has 'git' => (
53             is => 'ro',
54             isa => 'Git::Wrapper',
55             traits => ['NoGetopt'],
56             lazy => 1,
57             default => sub {
58             my ($self) = @_;
59             return Git::Wrapper->new(
60             $self->get_workdir()->absolute()->resolve()->stringify()
61             );
62             },
63             );
64              
65             has 'logger' => (
66             is => 'ro',
67             isa => 'Log::Dispatchouli',
68             traits => ['NoGetopt'],
69             lazy => 1,
70             default => sub {
71             my ($self) = @_;
72             my $logger = Log::Dispatchouli->new({
73             ident => 'environment-updater',
74             to_stderr => 1,
75             log_pid => 0,
76             });
77             $logger->set_prefix('[update-'.$self->get_environment().'] ');
78             return $logger;
79             },
80             );
81              
82              
83              
84             my ($self, $prefix) = @_;
85              
86 11     11 1 2391 return $self->get_logger()->proxy({
87             proxy_prefix => $prefix,
88 11         783 });
89             }
90              
91              
92              
93             my ($self) = @_;
94              
95             if ($self->get_git()->status()->is_dirty()) {
96 1     1 1 36 $self->get_logger()->log_fatal(BOLD.RED."Dirty sandbox, aborting".RESET);
97             }
98 1 50       77  
99 0         0 try {
100             $self->get_logger()->log(CYAN.'Fetching latest changes from '.$self->get_remote().'...'.RESET);
101             $self->get_git()->fetch($self->get_remote());
102              
103 1     1   282 for my $branch ($self->get_from(), $self->get_environment()) {
104 1         623 unless (any { $_ eq $branch } $self->get_local_branches()) {
105             $self->create_and_switch_to_branch($branch);
106 1         33685 }
107 2 100       76 $self->update_branch($branch);
  5         33  
108 1         28 }
109              
110 2         106 $self->merge($self->get_from(), $self->get_environment());
111              
112             $self->update_submodules();
113 1         141  
114             $self->get_logger()->log(
115 1         26 BOLD.GREEN.'Done. Please check the changes and '
116             . '"git push '.$self->get_remote().' '.$self->get_environment().'"'
117 1         296 .' them.'.RESET
118             );
119             }
120             catch {
121             my $error = $_;
122             $error =~ s{^(?:\[.*\]\s)*}{}x; # Remove prefixes
123             $self->get_logger()->log(BOLD.RED.'Failed to update. Error was: '.RESET.$error);
124 0     0   0 };
125 0         0  
126 0         0 return;
127 1         13832 }
128              
129 1         342  
130              
131             my ($self) = @_;
132              
133             my @branches;
134             for my $branch ($self->get_git()->branch()) {
135 4     4 1 54 $branch =~ s{^[*\s]*}{}x;
136             push @branches, $branch;
137 4         18 }
138 4         346  
139 8         29523 return @branches;
140 8         57 }
141              
142              
143 4         126  
144             my ($self, $branch) = @_;
145              
146             return $self->get_remote().'/'.$branch;
147             }
148              
149 13     13 1 299  
150              
151 13         648 my ($self, $branch) = @_;
152              
153             $self->get_proxy_logger(BLUE.'[create-branch] '.RESET)->log(
154             'Creating local branch '.$branch.' from '.$self->remote_branch_for($branch).'...'
155             );
156             $self->get_git()->checkout('-b', $branch, $self->remote_branch_for($branch));
157 3     3 1 58  
158             return;
159 3         170 }
160              
161              
162 3         2316  
163             my ($self, $branch) = @_;
164 3         31664  
165             my $logger = $self->get_proxy_logger(YELLOW.'[update-branch] '.RESET);
166              
167             my $remote_branch = $self->remote_branch_for($branch);
168             $logger->log(
169             'Updating local branch '.$branch.' from '.$remote_branch.'...'
170 5     5 1 73 );
171             $self->get_git()->checkout($branch);
172 5         400 try {
173             $logger->log($self->get_git()->merge('--ff-only', $remote_branch));
174 5         450 }
175 5         104 catch {
176             chomp;
177             $logger->log_fatal(
178 5         3275 "$_ - does ".$remote_branch.' exist and is local branch '
179             .$branch.' not diverged from it?'
180 5     5   1251 );
181             };
182              
183 0     0   0 return;
184 0         0 }
185              
186              
187              
188 5         44975 my ($self, $from, $to) = @_;
189              
190 5         53534 my $logger = $self->get_proxy_logger(MAGENTA.'[merge] '.RESET);
191             $logger->log('Merging '.$from.' into '.$to.'...');
192             $self->get_git()->checkout($to);
193             $logger->log($self->get_git()->merge('--no-ff', $from));
194             return;
195             }
196 2     2 1 20  
197              
198 2         89  
199 2         248 my ($self) = @_;
200 2         648  
201 2         19729 my $workdir = pushd($self->get_workdir());
202 2         31378 my $logger = $self->get_proxy_logger(YELLOW.'[update-submodules] '.RESET);
203             $logger->log('Updating submodules...');
204             if (my @updated = $self->get_git()->submodule('update', '--init')) {
205             $logger->log($_) for @updated;
206             }
207             else {
208 1     1 1 14 $logger->log('No submodules to update.');
209             }
210 1         133  
211 1         388 return;
212 1         58 }
213 1 50       386  
214 0         0 __PACKAGE__->meta()->make_immutable();
215              
216             1;
217 1         40586  
218              
219             =pod
220 1         865  
221             =encoding UTF-8
222              
223             =head1 NAME
224              
225             App::Puppet::Environment::Updater - Update a Puppet environment in a Git branch
226              
227             =head1 VERSION
228              
229             version 0.001002
230              
231             =head1 SYNOPSIS
232              
233             use App::Puppet::Environment::Updater;
234              
235             App::Puppet::Environment::Updater->new_with_options()->run();
236              
237             =head1 DESCRIPTION
238              
239             App::Puppet::Environment::Updater is intended to update Puppet environments which
240             are in Git branches. There are many ways to organize a Puppet setup and Puppet
241             environments, and this application supports the following approach:
242              
243             =over
244              
245             =item *
246              
247             There is one Git repository with four branches, each of which represents a
248             Puppet environment:
249              
250             =over
251              
252             =item *
253              
254             C<development>
255              
256             =item *
257              
258             C<test>
259              
260             =item *
261              
262             C<staging>
263              
264             =item *
265              
266             C<production>
267              
268             =back
269              
270             =item *
271              
272             Each branch contains a C<site.pp> with the Puppet nodes that are present in the
273             environment represented by the branch.
274              
275             =item *
276              
277             Puppet modules are included as Git submodules, usually below C<modules>. It's not
278             necessary to use Git submodules, but it simplifies reuse of the Puppet modules in
279             other projects.
280              
281             =back
282              
283             The sandbox of the Git repository usually looks about as follows:
284              
285             .
286             |-- modules
287             | |-- module1
288             | | |-- manifests
289             | | | `-- init.pp
290             | | `-- templates
291             | | `-- template1.erb
292             | `-- module2
293             | |-- files
294             | | `-- file1.pl
295             | `-- manifests
296             | `-- init.pp
297             `-- site.pp
298              
299             In order to move a change from eg. C<development> to C<testing>, one can usually
300             simply merge the C<development> branch into the C<testing> branch and update the
301             submodules. This application tries to automate this and work around some of the
302             pitfalls that exist on the way.
303              
304             =head1 METHODS
305              
306             =head2 new
307              
308             Constructor, creates new instance of the application.
309              
310             =head3 Parameters
311              
312             This method expects its parameters as a hash reference. See C<--usage> to see
313             which parameters can be passed on the command line.
314              
315             =over
316              
317             =item from
318              
319             The branch to merge from.
320              
321             =item environment
322              
323             The branch to merge to.
324              
325             =item remote
326              
327             The Git remote where changes can be fetched from and should be pushed to. B<This
328             application does currently not push any changes.> Defaults to C<origin>.
329              
330             =item workdir
331              
332             Directory with the Git sandbox that should be used. Defaults to the current
333             directory, but should point to the toplevel of the working tree.
334              
335             =item git
336              
337             The L<Git::Wrapper|Git::Wrapper> instance to use.
338              
339             =item logger
340              
341             The L<Log::Dispatchouli|Log::Dispatchouli> instance to use.
342              
343             =back
344              
345             =head2 get_proxy_logger
346              
347             Get a proxy logger with a given prefix.
348              
349             =head3 Parameters
350              
351             This method expects positional parameters.
352              
353             =over
354              
355             =item prefix
356              
357             A prefix which should be set in the proxy logger.
358              
359             =back
360              
361             =head3 Result
362              
363             A L<Log::Dispatchouli::Proxy|Log::Dispatchouli::Proxy> instance.
364              
365             =head2 run
366              
367             Run the application.
368              
369             =head3 Result
370              
371             Nothing on success, an exception otherwise.
372              
373             =head2 get_local_branches
374              
375             Get a list with local branches.
376              
377             =head3 Result
378              
379             The local branches.
380              
381             =head2 remote_branch_for
382              
383             Construct the name of a remote branch given a branch name.
384              
385             =head3 Parameters
386              
387             This method expects positional parameters.
388              
389             =over
390              
391             =item branch
392              
393             Name of the branch the remote branch name should be constructed for.
394              
395             =back
396              
397             =head3 Result
398              
399             The name of the remote branch.
400              
401             =head2 create_and_switch_to_branch
402              
403             Create a local branch starting at the corresponding remote branch and switch to
404             it.
405              
406             =head3 Parameters
407              
408             This method expects positional parameters.
409              
410             =over
411              
412             =item branch
413              
414             Name of the branch.
415              
416             =back
417              
418             =head3 Result
419              
420             Nothing on success, an exception otherwise.
421              
422             =head2 update_branch
423              
424             Update a local branch from the corresponding remote branch, using a fast-forward
425             merge.
426              
427             =head3 Parameters
428              
429             This method expects positional parameters.
430              
431             =over
432              
433             =item branch
434              
435             The name of the branch which should be updated.
436              
437             =back
438              
439             =head3 Result
440              
441             Nothing on success, an exception otherwise.
442              
443             =head2 merge
444              
445             Merge a given branch into another branch.
446              
447             =head3 Parameters
448              
449             This method expects positional parameters.
450              
451             =over
452              
453             =item from
454              
455             The branch to merge from.
456              
457             =item to
458              
459             The branch to merge to.
460              
461             =back
462              
463             =head3 Result
464              
465             Nothing on success, an exception otherwise.
466              
467             =head2 update_submodules
468              
469             Update the submodules.
470              
471             =head3 Result
472              
473             Nothing on success, an exception otherwise.
474              
475             =head1 SEE ALSO
476              
477             =over
478              
479             =item *
480              
481             L<http://www.puppetlabs.com/> - Puppet
482              
483             =item *
484              
485             L<http://docs.puppetlabs.com/guides/environment.html> - How to configure Puppet
486             environments.
487              
488             =item *
489              
490             L<http://git-scm.com/> - Git
491              
492             =back
493              
494             =head1 AUTHOR
495              
496             Manfred Stock <mstock@cpan.org>
497              
498             =head1 COPYRIGHT AND LICENSE
499              
500             This software is copyright (c) 2022 by Manfred Stock.
501              
502             This is free software; you can redistribute it and/or modify it under
503             the same terms as the Perl 5 programming language system itself.
504              
505             =cut