From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

$App::Puppet::Environment::Updater::VERSION = '0.001002';
# ABSTRACT: Update a Puppet environment in a Git branch
use Moose;
use Carp;
use List::MoreUtils qw(any);
use Term::ANSIColor qw(:constants);
has 'from' => (
is => 'ro',
isa => 'Str',
required => 1,
documentation => 'Environment/branch which should be merged',
);
has 'environment' => (
is => 'ro',
isa => 'Str',
required => 1,
documentation => 'Environment/branch which should be updated',
);
has 'remote' => (
is => 'ro',
isa => 'Str',
documentation => 'Git remote to fetch latest changes from, defaults to "origin"',
default => 'origin',
);
has 'workdir' => (
is => 'ro',
isa => 'Path::Class::Dir',
coerce => 1,
documentation => 'Directory to work in, should be the directory with the environment that should be updated',
default => sub {
return Path::Class::Dir->new('.')->absolute();
},
);
has 'git' => (
is => 'ro',
isa => 'Git::Wrapper',
traits => ['NoGetopt'],
lazy => 1,
default => sub {
my ($self) = @_;
return Git::Wrapper->new(
$self->get_workdir()->absolute()->resolve()->stringify()
);
},
);
has 'logger' => (
is => 'ro',
isa => 'Log::Dispatchouli',
traits => ['NoGetopt'],
lazy => 1,
default => sub {
my ($self) = @_;
my $logger = Log::Dispatchouli->new({
ident => 'environment-updater',
to_stderr => 1,
log_pid => 0,
});
$logger->set_prefix('[update-'.$self->get_environment().'] ');
return $logger;
},
);
sub get_proxy_logger {
my ($self, $prefix) = @_;
return $self->get_logger()->proxy({
proxy_prefix => $prefix,
});
}
sub run {
my ($self) = @_;
if ($self->get_git()->status()->is_dirty()) {
$self->get_logger()->log_fatal(BOLD.RED."Dirty sandbox, aborting".RESET);
}
try {
$self->get_logger()->log(CYAN.'Fetching latest changes from '.$self->get_remote().'...'.RESET);
$self->get_git()->fetch($self->get_remote());
for my $branch ($self->get_from(), $self->get_environment()) {
unless (any { $_ eq $branch } $self->get_local_branches()) {
$self->create_and_switch_to_branch($branch);
}
$self->update_branch($branch);
}
$self->merge($self->get_from(), $self->get_environment());
$self->update_submodules();
$self->get_logger()->log(
BOLD.GREEN.'Done. Please check the changes and '
. '"git push '.$self->get_remote().' '.$self->get_environment().'"'
.' them.'.RESET
);
}
catch {
my $error = $_;
$error =~ s{^(?:\[.*\]\s)*}{}x; # Remove prefixes
$self->get_logger()->log(BOLD.RED.'Failed to update. Error was: '.RESET.$error);
};
return;
}
sub get_local_branches {
my ($self) = @_;
my @branches;
for my $branch ($self->get_git()->branch()) {
$branch =~ s{^[*\s]*}{}x;
push @branches, $branch;
}
return @branches;
}
sub remote_branch_for {
my ($self, $branch) = @_;
return $self->get_remote().'/'.$branch;
}
sub create_and_switch_to_branch {
my ($self, $branch) = @_;
$self->get_proxy_logger(BLUE.'[create-branch] '.RESET)->log(
'Creating local branch '.$branch.' from '.$self->remote_branch_for($branch).'...'
);
$self->get_git()->checkout('-b', $branch, $self->remote_branch_for($branch));
return;
}
sub update_branch {
my ($self, $branch) = @_;
my $logger = $self->get_proxy_logger(YELLOW.'[update-branch] '.RESET);
my $remote_branch = $self->remote_branch_for($branch);
$logger->log(
'Updating local branch '.$branch.' from '.$remote_branch.'...'
);
$self->get_git()->checkout($branch);
try {
$logger->log($self->get_git()->merge('--ff-only', $remote_branch));
}
catch {
chomp;
$logger->log_fatal(
"$_ - does ".$remote_branch.' exist and is local branch '
.$branch.' not diverged from it?'
);
};
return;
}
sub merge {
my ($self, $from, $to) = @_;
my $logger = $self->get_proxy_logger(MAGENTA.'[merge] '.RESET);
$logger->log('Merging '.$from.' into '.$to.'...');
$self->get_git()->checkout($to);
$logger->log($self->get_git()->merge('--no-ff', $from));
return;
}
sub update_submodules {
my ($self) = @_;
my $workdir = pushd($self->get_workdir());
my $logger = $self->get_proxy_logger(YELLOW.'[update-submodules] '.RESET);
$logger->log('Updating submodules...');
if (my @updated = $self->get_git()->submodule('update', '--init')) {
$logger->log($_) for @updated;
}
else {
$logger->log('No submodules to update.');
}
return;
}
__PACKAGE__->meta()->make_immutable();
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
App::Puppet::Environment::Updater - Update a Puppet environment in a Git branch
=head1 VERSION
version 0.001002
=head1 SYNOPSIS
use App::Puppet::Environment::Updater;
App::Puppet::Environment::Updater->new_with_options()->run();
=head1 DESCRIPTION
App::Puppet::Environment::Updater is intended to update Puppet environments which
are in Git branches. There are many ways to organize a Puppet setup and Puppet
environments, and this application supports the following approach:
=over
=item *
There is one Git repository with four branches, each of which represents a
Puppet environment:
=over
=item *
C<development>
=item *
C<test>
=item *
C<staging>
=item *
C<production>
=back
=item *
Each branch contains a C<site.pp> with the Puppet nodes that are present in the
environment represented by the branch.
=item *
Puppet modules are included as Git submodules, usually below C<modules>. It's not
necessary to use Git submodules, but it simplifies reuse of the Puppet modules in
other projects.
=back
The sandbox of the Git repository usually looks about as follows:
.
|-- modules
| |-- module1
| | |-- manifests
| | | `-- init.pp
| | `-- templates
| | `-- template1.erb
| `-- module2
| |-- files
| | `-- file1.pl
| `-- manifests
| `-- init.pp
`-- site.pp
In order to move a change from eg. C<development> to C<testing>, one can usually
simply merge the C<development> branch into the C<testing> branch and update the
submodules. This application tries to automate this and work around some of the
pitfalls that exist on the way.
=head1 METHODS
=head2 new
Constructor, creates new instance of the application.
=head3 Parameters
This method expects its parameters as a hash reference. See C<--usage> to see
which parameters can be passed on the command line.
=over
=item from
The branch to merge from.
=item environment
The branch to merge to.
=item remote
The Git remote where changes can be fetched from and should be pushed to. B<This
application does currently not push any changes.> Defaults to C<origin>.
=item workdir
Directory with the Git sandbox that should be used. Defaults to the current
directory, but should point to the toplevel of the working tree.
=item git
The L<Git::Wrapper|Git::Wrapper> instance to use.
=item logger
The L<Log::Dispatchouli|Log::Dispatchouli> instance to use.
=back
=head2 get_proxy_logger
Get a proxy logger with a given prefix.
=head3 Parameters
This method expects positional parameters.
=over
=item prefix
A prefix which should be set in the proxy logger.
=back
=head3 Result
A L<Log::Dispatchouli::Proxy|Log::Dispatchouli::Proxy> instance.
=head2 run
Run the application.
=head3 Result
Nothing on success, an exception otherwise.
=head2 get_local_branches
Get a list with local branches.
=head3 Result
The local branches.
=head2 remote_branch_for
Construct the name of a remote branch given a branch name.
=head3 Parameters
This method expects positional parameters.
=over
=item branch
Name of the branch the remote branch name should be constructed for.
=back
=head3 Result
The name of the remote branch.
=head2 create_and_switch_to_branch
Create a local branch starting at the corresponding remote branch and switch to
it.
=head3 Parameters
This method expects positional parameters.
=over
=item branch
Name of the branch.
=back
=head3 Result
Nothing on success, an exception otherwise.
=head2 update_branch
Update a local branch from the corresponding remote branch, using a fast-forward
merge.
=head3 Parameters
This method expects positional parameters.
=over
=item branch
The name of the branch which should be updated.
=back
=head3 Result
Nothing on success, an exception otherwise.
=head2 merge
Merge a given branch into another branch.
=head3 Parameters
This method expects positional parameters.
=over
=item from
The branch to merge from.
=item to
The branch to merge to.
=back
=head3 Result
Nothing on success, an exception otherwise.
=head2 update_submodules
Update the submodules.
=head3 Result
Nothing on success, an exception otherwise.
=head1 SEE ALSO
=over
=item *
=item *
environments.
=item *
=back
=head1 AUTHOR
Manfred Stock <mstock@cpan.org>
=head1 COPYRIGHT AND LICENSE
This software is copyright (c) 2022 by Manfred Stock.
This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.
=cut