view release on metacpan or search on metacpan
Revision history for AWS::CLIWrapper
1.27 2023-06-29
[IMPROVEMENTS]
- Optionally catch aws-cli errors and retry (PR #23 by @nohuhu)
- Add servics with aws-cli/1.27.163
1.26 2023-05-25
[IMPROVEMENTS]
- Add `region` method to allow introspection on constructor arguments (PR #22 by @nohuhu)
1.25 2023-03-16
[BUG FIXES]
- Fix AWS_CLIWRAPPER_TIMEOUT test (PR #20 by @nohuhu)
[IMPROVEMENTS]
- Allow overriding aws-cli execution timeout via environment variable (PR #19 by @nohuhu)
- Add servics with aws-cli/1.27.91
1.23 2022-03-23
[IMPROVEMENTS]
- Fix test suite fails with aws-cli v2 (rt 141885)
1.22 2022-03-17
[IMPROVEMENTS]
- optionally croak() on errors (PR #18 by @nohuhu)
- Add servics with aws-cli/1.22.76
1.21 2021-05-20
[IMPROVEMENTS]
- Add servics with aws-cli/1.19.76
1.20 2021-02-12
[IMPROVEMENTS]
- Add servics with aws-cli/1.19.6
cpanfile
lib/AWS/CLIWrapper.pm
Makefile.PL
MANIFEST This list of files
t/00_compile.t
t/01_new.t
t/03_awscli_path.t
t/03_awscli_timeout.t
t/03_awscli_version.t
t/03_region.t
t/04_errors.t
t/05_catch_error_options.t
t/06_catch_errors.t
t/bin/mock-aws
xt/01_podspell.t
xt/02_perlcritic.t
xt/03_pod.t
xt/05_dependencies.t
xt/10_ec2.t
xt/11_struct-in-list.t
xt/12_nested-boolean.t
xt/19_error.t
xt/20_s3-sync.t
xt/30_compat.t
xt/90_dependencies.t
xt/91_usedmodules.t
xt/92_usedfunctions.t
xt/perlcriticrc
META.yml Module YAML meta-data (added by MakeMaker)
META.json Module JSON meta-data (added by MakeMaker)
lib/AWS/CLIWrapper.pm view on Meta::CPAN
push @opt, param2opt($k, $v);
}
}
my $self = bless {
region => $region,
opt => \@opt,
json => JSON->new,
param => \%param,
awscli_path => $param{awscli_path} || 'aws',
croak_on_error => !!$param{croak_on_error},
timeout => (defined $ENV{AWS_CLIWRAPPER_TIMEOUT}) ? $ENV{AWS_CLIWRAPPER_TIMEOUT} : 30,
}, $class;
return $self;
}
sub region { shift->{region} }
sub awscli_path {
my ($self) = @_;
lib/AWS/CLIWrapper.pm view on Meta::CPAN
$v = $1;
} else {
$v = 0;
}
version->parse($v);
};
}
return $AWSCLI_VERSION;
}
sub catch_error_pattern {
my ($self) = @_;
return $ENV{AWS_CLIWRAPPER_CATCH_ERROR_PATTERN}
if defined $ENV{AWS_CLIWRAPPER_CATCH_ERROR_PATTERN};
return $self->{param}->{catch_error_pattern}
if defined $self->{param}->{catch_error_pattern};
return;
}
sub catch_error_retries {
my ($self) = @_;
my $retries = defined $ENV{AWS_CLIWRAPPER_CATCH_ERROR_RETRIES}
? $ENV{AWS_CLIWRAPPER_CATCH_ERROR_RETRIES}
: defined $self->{param}->{catch_error_retries}
? $self->{param}->{catch_error_retries}
: $DEFAULT_CATCH_ERROR_RETRIES;
$retries = $DEFAULT_CATCH_ERROR_RETRIES if $retries < 0;
return $retries;
}
sub catch_error_min_delay {
my ($self) = @_;
my $min_delay = defined $ENV{AWS_CLIWRAPPER_CATCH_ERROR_MIN_DELAY}
? $ENV{AWS_CLIWRAPPER_CATCH_ERROR_MIN_DELAY}
: defined $self->{param}->{catch_error_min_delay}
? $self->{param}->{catch_error_min_delay}
: $DEFAULT_CATCH_ERROR_MIN_DELAY;
$min_delay = $DEFAULT_CATCH_ERROR_MIN_DELAY if $min_delay < 0;
return $min_delay;
}
sub catch_error_max_delay {
my ($self) = @_;
my $min_delay = $self->catch_error_min_delay;
my $max_delay = defined $ENV{AWS_CLIWRAPPER_CATCH_ERROR_MAX_DELAY}
? $ENV{AWS_CLIWRAPPER_CATCH_ERROR_MAX_DELAY}
: defined $self->{param}->{catch_error_max_delay}
? $self->{param}->{catch_error_max_delay}
: $DEFAULT_CATCH_ERROR_MAX_DELAY;
$max_delay = $DEFAULT_CATCH_ERROR_MAX_DELAY if $max_delay < 0;
$max_delay = $min_delay if $min_delay > $max_delay;
return $max_delay;
}
sub catch_error_delay {
my ($self) = @_;
my $min = $self->catch_error_min_delay;
my $max = $self->catch_error_max_delay;
return $min == $max ? $min : $min + (int rand $max - $min);
}
sub param2opt {
my($k, $v) = @_;
my @v;
$k =~ s/_/-/g;
lib/AWS/CLIWrapper.pm view on Meta::CPAN
my @o = param2opt($k, $v);
if ($service eq 's3' && $k =~ /^(?:include|exclude)$/) {
my $optk = shift @o;
@o = map { $optk => $_ } @o;
}
push @cmd, @o;
}
@cmd = map { shell_quote($_) } @cmd;
warn "cmd: ".join(' ', @cmd) if $ENV{AWSCLI_DEBUG};
my $error_re = $self->catch_error_pattern;
my $retries = $error_re ? $self->catch_error_retries : 0;
RETRY: {
$Error = { Message => '', Code => '' };
my $exit_value = $self->_run(\%opt, \@cmd);
my $ret = $self->_handle($service, $operation, $exit_value);
return $ret unless $Error->{Code};
if ($retries-- > 0 and $Error->{Message} =~ $error_re) {
my $delay = $self->catch_error_delay;
warn "Caught error matching $error_re, sleeping $delay seconds before retrying\n"
if $ENV{AWSCLI_DEBUG};
sleep $delay;
redo RETRY;
}
croak $Error->{Message} if $self->{croak_on_error};
return $ret;
}
}
sub _run {
my ($self, $opt, $cmd) = @_;
my $ret;
if (exists $opt->{'nofork'} && $opt->{'nofork'}) {
lib/AWS/CLIWrapper.pm view on Meta::CPAN
Constructor of AWS::CLIWrapper. Acceptable AWS CLI params are:
region region_name:Str
profile profile_name:Str
endpoint_url endpoint_url:Str
Additionally, the these params can be used to control the wrapper behavior:
nofork Truthy to avoid forking when executing `aws`
timeout `aws` execution timeout
croak_on_error Truthy to croak() with the error message when `aws`
exits with non-zero code
catch_error_pattern Regexp pattern to match for error handling.
catch_error_retries Retries for handling errors.
catch_error_min_delay Minimal delay before retrying `aws` call
when an error was caught.
catch_error_max_delay Maximal delay before retrying `aws` call.
See below for more detailed explanation.
=item B<accessanalyzer>($operation:Str, $param:HashRef, %opt:Hash)
=item B<account>($operation:Str, $param:HashRef, %opt:Hash)
=item B<acm>($operation:Str, $param:HashRef, %opt:Hash)
=item B<acm_pca>($operation:Str, $param:HashRef, %opt:Hash)
lib/AWS/CLIWrapper.pm view on Meta::CPAN
Third arg "opt" is optional. Available key/values are below:
timeout => Int
Maximum time the "aws" command is allowed to run before aborting.
default is 30 seconds, unless overridden with AWS_CLIWRAPPER_TIMEOUT environment variable.
nofork => Int (>0)
Call IPC::Cmd::run vs. IPC::Cmd::run_forked (mostly useful if/when in perl debugger). Note: 'timeout', if used with 'nofork', will merely cause an alarm and return. ie. 'run' will NOT kill the awscli command like 'run_forked' will.
croak_on_error => Int (>0)
When set to a truthy value, this will make AWS::CLIWrapper to croak() with error message when `aws` command exits with non-zero status. Default behavior is to set $AWS::CLIWrapper::Error and return.
catch_error_pattern => RegExp
When defined, this option will enable catching `aws-cli` errors matching this pattern
and retrying `aws-cli` command execution. Environment variable
AWS_CLIWRAPPER_CATCH_ERROR_PATTERN takes precedence over this option, if both
are defined.
Default is undef.
catch_error_retries => Int (>= 0)
When defined, this option will set the number of retries to make when `aws-cli` error
was caught with catch_error_pattern, before giving up. Environment variable
AWS_CLIWRAPPER_CATCH_ERROR_RETRIES takes precedence over this option, if both
are defined.
0 (zero) retries is a valid way to turn off error catching via environment variable
in certain scenarios. Negative values are invalid and will be reset to default.
Default is 3.
catch_error_min_delay => Int (>= 0)
When defined, this option will set the minimum delay in seconds before attempting
a retry of failed `aws-cli` execution when the error was caught. Environment variable
AWS_CLIWRAPPER_CATCH_ERROR_MIN_DELAY takes precedence over this option, if both
are defined.
0 (zero) is a valid value. Negative values are invalid and will be reset to default.
Default is 3.
catch_error_max_delay => Int (>= 0)
When defined, this option will set the maximum delay in seconds before attempting
a retry of failed `aws-cli` execution. Environment variable AWS_CLIWRAPPER_CATCH_ERROR_MAX_DELAY
takes precedence over this option, if both are defined.
0 (zero) is a valid value. Negative values are invalid and will be reset to default.
If catch_error_min_delay is greater than catch_error_max_delay, both are set
to catch_error_min_delay value.
Default is 10.
=back
=head1 ENVIRONMENT
=over 4
=item HOME: used by default by /usr/bin/aws utility to find it's credentials (if none are specified)
lib/AWS/CLIWrapper.pm view on Meta::CPAN
invocation of `aws-cli` that does not have a timeout value provided in the options argument of the
called function.
=item AWS_CLIWRAPPER_CATCH_ERROR_PATTERN
If this variable is set, AWS::CLIWrapper will retry `aws-cli` execution if stdout output
of failed `aws-cli` command matches the pattern. See L<ERROR HANDLING>.
=item AWS_CLIWRAPPER_CATCH_ERROR_RETRIES
How many times to retry command execution if an error was caught. Default is 3.
=item AWS_CLIWRAPPER_CATCH_ERROR_MIN_DELAY
Minimal delay before retrying command execution if an error was caught, in seconds.
Default is 3.
=item AWS_CLIWRAPPER_CATCH_ERROR_MAX_DELAY
Maximal delay before retrying command execution, in seconds. Default is 10.
=item AWS_CONFIG_FILE
=item AWS_ACCESS_KEY_ID
lib/AWS/CLIWrapper.pm view on Meta::CPAN
=item AWS_DEFAULT_REGION
See documents of aws-cli.
=back
=head1 ERROR HANDLING
=over 4
By default, when `aws-cli` exits with an error code (> 0), AWS::CLIWrapper will set
the error code and message to $AWS::CLIWrapper::Error (and optionally croak), thus
relaying the error to calling code. While this approach is beneficial 99% of the time,
in some use cases `aws-cli` execution fails for a temporary reason unrelated to
both calling code and AWS::CLIWrapper, and can be safely retried after a short delay.
One of this use cases is executing `aws-cli` on AWS EC2 instances, where `aws-cli`
retrieves its configuration and credentials from the API exposed to the EC2 instance;
at certain times these credentials may be rotated and calling `aws-cli` at exactly
the right moment will cause it to fail with `Unable to locate credentials` error.
To prevent this kind of errors from failing the calling code, AWS::CLIWrapper allows
configuring an RegExp pattern and retry `aws-cli` execution if it fails with an error
matching the configured pattern.
The error catching pattern, as well as other configuration, can be defined either
as AWS::CLIWrapper options in the code, or as respective environment variables
(see L<ENVIRONMENT>).
The actual delay before retrying a failed `aws-cli` execution is computed as a
random value of seconds between catch_error_min_delay (default 3) and catch_error_max_delay
(default 10). Backoff is not supported at this moment.
=back
=head1 AUTHOR
HIROSE Masaaki E<lt>hirose31 _at_ gmail.comE<gt>
=head1 REPOSITORY
t/04_errors.t view on Meta::CPAN
use strict;
use Test::More;
use AWS::CLIWrapper;
# Default error handling
my $aws = AWS::CLIWrapper->new;
if ($aws->awscli_version == 0) {
plan skip_all => 'not found aws command';
} else {
plan tests => 4;
}
my $res = $aws->elbv2();
is $res, undef, "default result is undefined";
# Is this a TODO?
is $AWS::CLIWrapper::Error->{Code}, "Unknown", "default error code match";
my $want_err_msg = qr!exited with code \[\d+\]
stderr:
.*
usage: aws \[options\] <command> <subcommand> \[<subcommand> ...\] \[parameters\]
To see help text, you can run:
aws help
aws <command> help
aws <command> <subcommand> help
!ms;
like $AWS::CLIWrapper::Error->{Message}, $want_err_msg, "default error message match";
# Croaking
my $aws_croak = AWS::CLIWrapper->new(croak_on_error => 1);
eval {
$aws_croak->elbv2();
};
like $@, $want_err_msg, "croak on error message match";
t/05_catch_error_options.t view on Meta::CPAN
done_testing;
__DATA__
# line 41
{
'mock-aws version' => {
method => 'awscli_version',
want => '2.42.4242',
},
'default-catch_error_pattern' => {
method => 'catch_error_pattern',
want => undef,
},
'default-catch_error_retries' => {
method => 'catch_error_retries',
want => 3,
},
'default-catch_error_min_delay' => {
method => 'catch_error_min_delay',
want => 3,
},
'default-catch_error_max_delay' => {
method => 'catch_error_max_delay',
want => 10,
},
'default-catch_error_delay' => {
method => 'catch_error_delay',
want => [['>=', 3], ['<=', 10]],
},
'env-catch_error_pattern' => {
env => {
AWS_CLIWRAPPER_CATCH_ERROR_PATTERN => 'foo',
},
method => 'catch_error_pattern',
want => 'foo',
},
'env-catch_error_retries' => {
env => {
AWS_CLIWRAPPER_CATCH_ERROR_RETRIES => 10,
},
method => 'catch_error_retries',
want => 10,
},
'env-catch_error_retries-invalid' => {
env => {
AWS_CLIWRAPPER_CATCH_ERROR_RETRIES => -10,
},
method => 'catch_error_retries',
want => 3,
},
'env-catch_error_min_delay' => {
env => {
AWS_CLIWRAPPER_CATCH_ERROR_MIN_DELAY => 15,
},
method => 'catch_error_min_delay',
want => 15,
},
'env-catch_error_min_delay-invalid' => {
env => {
AWS_CLIWRAPPER_CATCH_ERROR_MIN_DELAY => -15,
},
method => 'catch_error_min_delay',
want => 3,
},
'env-catch_error_max_delay' => {
env => {
AWS_CLIWRAPPER_CATCH_ERROR_MAX_DELAY => 30,
},
method => 'catch_error_max_delay',
want => 30,
},
'env-catch_error_max_delay-invalid' => {
env => {
AWS_CLIWRAPPER_CATCH_ERROR_MAX_DELAY => -30,
},
method => 'catch_error_max_delay',
want => 10,
},
'env-catch_error_max_delay-gt-min_delay' => {
env => {
AWS_CLIWRAPPER_CATCH_ERROR_MIN_DELAY => 30,
AWS_CLIWRAPPER_CATCH_ERROR_MAX_DELAY => 15,
},
method => 'catch_error_max_delay',
want => 30,
},
'args-catch_error_pattern' => {
args => {
catch_error_pattern => 'bar',
},
method => 'catch_error_pattern',
want => 'bar',
},
'env-over-args-catch_error_pattern' => {
args => {
catch_error_pattern => 'qux',
},
env => {
AWS_CLIWRAPPER_CATCH_ERROR_PATTERN => 'baz',
},
method => 'catch_error_pattern',
want => 'baz',
},
'args-catch_error_retries' => {
args => {
catch_error_retries => 10,
},
method => 'catch_error_retries',
want => 10,
},
'env-over-args-catch_error_retries' => {
env => {
AWS_CLIWRAPPER_CATCH_ERROR_RETRIES => 20,
},
args => {
catch_error_retries => 10,
},
method => 'catch_error_retries',
want => 20,
},
'args-catch_error_min_delay' => {
args => {
catch_error_min_delay => 20,
},
method => 'catch_error_min_delay',
want => 20,
},
'env-over-args-catch_error_min_delay' => {
env => {
AWS_CLIWRAPPER_CATCH_ERROR_MIN_DELAY => 40,
},
args => {
catch_error_min_delay => 20,
},
method => 'catch_error_min_delay',
want => 40,
},
'args-catch_error_max_delay' => {
args => {
catch_error_max_delay => 60,
},
method => 'catch_error_max_delay',
want => 60,
},
'env-over-args-catch_error_max_delay' => {
env => {
AWS_CLIWRAPPER_CATCH_ERROR_MAX_DELAY => 120,
},
args => {
catch_error_max_delay => 60,
},
method => 'catch_error_max_delay',
want => 120,
},
'min-max-catch_error_delay' => {
args => {
catch_error_min_delay => 30,
catch_error_max_delay => 30,
},
method => 'catch_error_delay',
want => 30,
},
'zero-catch_error_delay' => {
args => {
catch_error_min_delay => 0,
catch_error_max_delay => 0,
},
method => 'catch_error_delay',
want => 0,
},
}
t/06_catch_errors.t view on Meta::CPAN
my ($wrapper_args, $env, $command, $subcommand, $cmd_args)
= @$test{qw(wrapper_args env command subcommand cmd_args)};
$env = {} unless $env;
my ($tmp_fh, $tmp_name) = tempfile;
print $tmp_fh $test->{retries} || 1;
close $tmp_fh;
local $ENV{AWS_CLIWRAPPER_TEST_ERROR_COUNTER_FILE} = $tmp_name;
local $ENV{AWS_CLIWRAPPER_TEST_DIE_WITH_ERROR} = $test->{error_to_die_with}
if $test->{error_to_die_with};
local @ENV{keys %$env} = values %$env;
$AWS::CLIWrapper::Error = { Message => '', Code => '' };
my $aws = AWS::CLIWrapper->new(%default_wrapper_args, %{$wrapper_args || {}});
my $res = eval { $aws->$command($subcommand, @{$cmd_args || []}) };
if ($test->{retries} > 0) {
open my $fh, "<", $tmp_name;
my $counter = <$fh>;
close $fh;
is $counter, 0, "$test_name retry counter exhausted";
}
like "$@", $test->{exception}, "$test_name exception";
like $AWS::CLIWrapper::Error->{Message}, $test->{error_msg_re},
"$test_name error message";
is_deeply $res, $test->{want}, "$test_name result";
}
done_testing;
__DATA__
# line 60
{
'no-error' => {
command => 'ecs',
subcommand => 'list-clusters',
error_to_die_with => undef,
error_msg_re => qr{^$},
exception => qr{^$},
want => {
clusterArns => [
"arn:aws:ecs:us-foo-1:123456789:cluster/foo",
"arn:aws:ecs:us-foo-1:123456789:cluster/bar",
"arn:aws:ecs:us-foo-1:123456789:cluster/baz"
],
}
},
'no-croak' => {
command => 'ecs',
subcommand => 'list-clusters',
error_to_die_with => 'uh-oh',
error_msg_re => qr{uh-oh},
exception => qr{^$},
want => undef,
},
'with-croak' => {
wrapper_args => { croak_on_error => 1 },
command => 'ecs',
subcommand => 'list-clusters',
error_to_die_with => 'foobaroo!',
error_msg_re => qr{foobaroo},
exception => qr{foobaroo},
want => undef,
},
'catch-no-croak' => {
env => {
AWS_CLIWRAPPER_CATCH_ERROR_PATTERN => 'FUBAR',
AWS_CLIWRAPPER_CATCH_ERROR_MIN_DELAY => 0,
AWS_CLIWRAPPER_CATCH_ERROR_MAX_DELAY => 0,
},
command => 'ecs',
subcommand => 'list-clusters',
error_to_die_with => 'FUBAR',
retries => 2,
error_msg_re => qr{^$},
exception => qr{^$},
want => {
clusterArns => [
"arn:aws:ecs:us-foo-1:123456789:cluster/foo",
"arn:aws:ecs:us-foo-1:123456789:cluster/bar",
"arn:aws:ecs:us-foo-1:123456789:cluster/baz"
],
}
},
'catch-with-croak' => {
env => {
AWS_CLIWRAPPER_CATCH_ERROR_PATTERN => 'throbbe',
AWS_CLIWRAPPER_CATCH_ERROR_MIN_DELAY => 0,
AWS_CLIWRAPPER_CATCH_ERROR_MAX_DELAY => 0,
},
command => 'ecs',
subcommand => 'list-clusters',
error_to_die_with => 'zong throbbe fung',
retries => 3,
error_msg_re => qr{^$},
exception => qr{^$},
want => {
clusterArns => [
"arn:aws:ecs:us-foo-1:123456789:cluster/foo",
"arn:aws:ecs:us-foo-1:123456789:cluster/bar",
"arn:aws:ecs:us-foo-1:123456789:cluster/baz"
],
}
},
}
t/bin/mock-aws view on Meta::CPAN
eval 'exec /usr/bin/perl -wS $0 ${1+"$@"}'
if 0;
use strict;
use warnings;
no warnings 'uninitialized';
version() if $ARGV[0] eq "--version";
handle_die_with_error() if $ENV{AWS_CLIWRAPPER_TEST_DIE_WITH_ERROR};
my $cmd = shift @ARGV;
my $subcmd = shift @ARGV;
handle($cmd, $subcmd);
sub handle {
my ($cmd, $subcmd) = @_;
$subcmd =~ s/-/_/g;
t/bin/mock-aws view on Meta::CPAN
if ('CODE' eq ref $handler) {
$handler->();
exit 0;
}
else {
help();
}
}
sub handle_die_with_error {
my $counter_file = $ENV{AWS_CLIWRAPPER_TEST_ERROR_COUNTER_FILE};
return unless -f $counter_file;
open my $fh, "<", $counter_file or die "Cannot open $counter_file for read: $!";
my $counter = <$fh>;
close $fh;
# This logic is the opposite of usual retries: we throw an error for the counter
# number of times and then proceed normally after.
if ($counter-- > 0) {
open $fh, ">", $counter_file or die "Cannot open $counter_file for write: $!";
print $fh $counter;
close $fh;
die $ENV{AWS_CLIWRAPPER_TEST_DIE_WITH_ERROR};
}
}
t/bin/mock-aws view on Meta::CPAN
sub help {
die <<__END__;
usage: aws [options] <command> <subcommand> [<subcommand> ...] [parameters]
To see help text, you can run:
aws help
aws <command> help
aws <command> <subcommand> help
aws: error: the following arguments are required: operation
__END__
}
sub ecs_list_clusters {
print <<__END__;
{
"clusterArns": [
"arn:aws:ecs:us-foo-1:123456789:cluster/foo",
"arn:aws:ecs:us-foo-1:123456789:cluster/bar",