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