AWS-Lambda-Quick

 view release on metacpan or  search on metacpan

README.md  view on Meta::CPAN


    #!/usr/bin/perl

    use strict;
    use warnings;

    use AWS::Lambda::Quick (
        name => 'hello-world',
    );

    sub handler {
        my $data = shift;
        my $name = $data->{queryStringParameters}{who} // "World";
        return {
            statusCode => 200,
            headers => {
                'Content-Type' => 'text/plain',
            },
            body => "Hello, $name",
        };
    }

README.md  view on Meta::CPAN


    use strict;
    use warnings;

    use JSON::PP;

    use AWS::Lambda::Quick (
        name => 'echo',
    );

    sub handler {
        my $data = shift;
        return {
            statusCode => 200,
            headers => {
                'Content-Type' => 'application/json',
            },
            body => encode_json($data),
        };
    }

cpanfile  view on Meta::CPAN

requires "AWS::CLIWrapper" => "0";
requires "Archive::Zip" => "0";
requires "File::Temp" => "0";
requires "JSON::PP" => "0";
requires "Mo" => "0";
requires "Path::Tiny" => "0";
requires "autodie" => "0";
requires "strict" => "0";
requires "warnings" => "0";

on 'test' => sub {
  requires "Exporter" => "0";
  requires "ExtUtils::MakeMaker" => "0";
  requires "File::Spec" => "0";
  requires "FindBin" => "0";
  requires "HTTP::Tiny" => "0";
  requires "Test2::V0" => "0";
  requires "Test::More" => "1.302015";
  requires "Test::TempDir::Tiny" => "0";
  requires "base" => "0";
  requires "lib" => "0";
};

on 'test' => sub {
  recommends "CPAN::Meta" => "2.120900";
};

on 'configure' => sub {
  requires "ExtUtils::MakeMaker" => "0";
};

on 'develop' => sub {
  requires "Code::TidyAll::Plugin::Test::Vars" => "0.02";
  requires "File::Spec" => "0";
  requires "IO::Handle" => "0";
  requires "IPC::Open3" => "0";
  requires "Parallel::ForkManager" => "1.19";
  requires "Perl::Critic" => "1.126";
  requires "Perl::Tidy" => "20160302";
  requires "Pod::Wordlist" => "0";
  requires "Test::CPAN::Changes" => "0.19";
  requires "Test::CPAN::Meta::JSON" => "0.16";

lib/AWS/Lambda/Quick.pm  view on Meta::CPAN

package AWS::Lambda::Quick;

use strict;
use warnings;
use autodie;

our $VERSION = '1.0002';

use AWS::Lambda::Quick::Processor ();

sub import {
    shift;

    # where's the source code of the script calling us?
    my ( undef, $file, undef ) = caller;

    # process the whole thing
    my $proc = AWS::Lambda::Quick::Processor->new(
        src_filename => $file,
        @_,
    );

lib/AWS/Lambda/Quick.pm  view on Meta::CPAN


    #!/usr/bin/perl

    use strict;
    use warnings;

    use AWS::Lambda::Quick (
        name => 'hello-world',
    );

    sub handler {
        my $data = shift;
        my $name = $data->{queryStringParameters}{who} // "World";
        return {
            statusCode => 200,
            headers => {
                'Content-Type' => 'text/plain',
            },
            body => "Hello, $name",
        };
    }

lib/AWS/Lambda/Quick.pm  view on Meta::CPAN


    use strict;
    use warnings;

    use JSON::PP;

    use AWS::Lambda::Quick (
        name => 'echo',
    );

    sub handler {
        my $data = shift;
        return {
            statusCode => 200,
            headers => {
                'Content-Type' => 'application/json',
            },
            body => encode_json($data),
        };
    }

lib/AWS/Lambda/Quick/CreateZip.pm  view on Meta::CPAN

our $VERSION = '1.0002';

use Archive::Zip qw( :ERROR_CODES :CONSTANTS );
use Path::Tiny qw( path );

has src_filename => required => 1;
has zip_filename => required => 1;

has extra_files => default => [];

has _src_path => sub { path( shift->src_filename ) };
has _src_dir  => sub { shift->_src_path->parent };
has _zip_class  => default => 'Archive::Zip';
has _zip        => sub { shift->_zip_class->new };
has _script_src => sub { shift->_src_path->slurp_raw };

# this is the same src as in script src but the first occurance of
# "use AWS::Lambda::Quick" is prepended with
# "$INC{'AWS/Lambda/Quick.pm'}=1" to prevent it actually being loaded
# from disk.  Note this happens on just one line to avoid screwing
# with line numebrs that could mess with error messages
has _converted_src => sub {
    my $self = shift;
    my $src  = $self->_script_src;
    $src =~ s{(?=use AWS::Lambda::Quick(?:\s|[;(]))}
             {BEGIN{\$INC{'AWS/Lambda/Quick.pm'}=1} };
    return $src;
};

### methods for interfacing with Archive::Zip
### no code outside this section should directly interact with the
### zip file

sub _add_string {
    my $self     = shift;
    my $string   = shift;
    my $filename = shift;

    my $zip           = $self->_zip;
    my $string_member = $zip->addString( $string, $filename );
    $string_member->desiredCompressionMethod(COMPRESSION_DEFLATED);
    return ();
}

sub _add_path {
    my $self = shift;
    my $path = path(shift);

    if ( $path->is_absolute ) {
        die "Cannot add absolute path! $path";
    }
    my $abs_path = path( $self->_src_dir, $path );

    # silently ignore files that don't exist.  This allows you
    # to say put extra_files => [qw( lib )] in your file and not

lib/AWS/Lambda/Quick/CreateZip.pm  view on Meta::CPAN

    return () unless -d $abs_path;
    my $iter = $abs_path->iterator;
    while ( my $next = $iter->() ) {
        my $child = $path->child( $next->basename );
        $self->_add_path($child);
    }

    return ();
}

sub _write_zip {
    my $self = shift;
    unless ( $self->_zip->writeToFileNamed( $self->zip_filename->stringify )
        == AZ_OK ) {
        die 'write error';
    }
    return ();
}

### logic for building the zip file contents ###

sub _build_zip {
    my $self = shift;
    $self->_add_string( $self->_converted_src, 'handler.pl' );
    $self->_add_path($_) for @{ $self->extra_files };
    return ();
}

sub create_zip {
    my $self = shift;
    $self->_build_zip;
    $self->_write_zip;
    return ();
}

1;

__END__

lib/AWS/Lambda/Quick/Processor.pm  view on Meta::CPAN

has src_filename => required => 1;

has 'description';
has 'extra_files';
has 'extra_layers';
has 'memory_size';
has 'region';
has 'stage_name';
has 'timeout';

has _tempdir => sub {
    return tempdir( CLEANUP => 1 );
};
has zip_filename => sub {
    return path( shift->_tempdir, 'handler.zip' );
};

sub selfkv {
    my $self = shift;
    my @computed_args;
    for my $key (@_) {
        my $val = $self->$key;
        push @computed_args, $key => $val if defined $val;
    }
    return @computed_args;
}

sub process {
    my $self = shift;

    AWS::Lambda::Quick::CreateZip->new(
        $self->selfkv(
            qw(
                extra_files
                src_filename
                zip_filename
                )
        ),

lib/AWS/Lambda/Quick/Upload.pm  view on Meta::CPAN


has extra_layers => default => [];
has region       => default => 'us-east-1';
has memory_size  => default => 128;           # this is the AWS default
has timeout      => default => 3;             # this is the AWS default
has description => default => 'A Perl AWS::Lambda::Quick Lambda function.';
has stage_name  => default => 'quick';

### lambda function computed attributes

has aws => sub {
    my $self = shift;

    return AWS::CLIWrapper->new(
        region => $self->region,
    );
};

has zip_file_blob => sub { 'fileb://' . shift->zip_filename };

# should we create the function from scratch or just update it?
# by default we interogate the api to see if it exists already
has update_type => sub {
    my $self = shift;
    my $aws  = $self->aws;

    my $result = $aws->lambda(
        'get-function',
        {
            'function-name' => $self->name,
        }
    );

    return $result ? 'update-function' : 'create-function';
};

### role attributes

has role      => default => 'perl-aws-lambda-quick';
has _role_arn => sub {
    my $self = shift;

    # if whatever we were passed in role was an actual ARN then we
    # can just use that without any further lookups
    if ( $self->role
        =~ /^arn:(aws[a-zA-Z-]*)?:iam::\d{12}:role\/?[a-zA-Z_0-9+=,.@\-_\/]+$/
    ) {
        $self->debug('using passed role arn');
        return $self->role;
    }

lib/AWS/Lambda/Quick/Upload.pm  view on Meta::CPAN

            'role-name' => $self->role,
        }
    );
    $self->debug('permissions attached to role');
    return $result->{Role}{Arn};
};

### rest api attributes

has rest_api    => default => 'perl-aws-lambda-quick';
has rest_api_id => sub {
    my $self = shift;

    # search existing apis
    $self->debug('searching for existing rest api');
    my $result = $self->aws_do(
        'apigateway',
        'get-rest-apis',
    );
    for ( @{ $result->{items} } ) {
        next unless $_->{name} eq $self->rest_api;

lib/AWS/Lambda/Quick/Upload.pm  view on Meta::CPAN

        {
            name => $self->rest_api,
            description =>
                'Created by AWS::Lambda::Quick. See https://metacpan.org/pod/AWS::Lambda::Quick for more info.',
        },
    );
    $self->debug('created new rest api');
    return $result->{id};
};

has resource_id => sub {
    my $self = shift;

    # TODO: We shold probably make this configurable, right?
    my $path = '/' . $self->name;

    # search existing resources
    $self->debug('searching of existing resource');
    my $result = $self->aws_do(
        'apigateway',
        'get-resources',

lib/AWS/Lambda/Quick/Upload.pm  view on Meta::CPAN

        {
            'rest-api-id' => $self->rest_api_id,
            'parent-id'   => $parent_id,
            'path-part'   => $self->name,
        },
    );
    $self->debug('created new resource');
    return $result->{id};
};

has greedy_resource_id => sub {
    my $self = shift;

    my $path = '/' . $self->name . '/{proxy+}';

    # search existing resources
    $self->debug('searching of existing greedy resource');
    my $result = $self->aws_do(
        'apigateway',
        'get-resources',
        {

lib/AWS/Lambda/Quick/Upload.pm  view on Meta::CPAN

            'parent-id'   => $self->resource_id,
            'path-part'   => '{proxy+}',
        },
    );
    $self->debug('created new greedy resource');
    return $result->{id};
};

### methods

sub upload {
    my $self = shift;

    my $function_arn = $self->_upload_function;

    for my $resource_id ( $self->resource_id, $self->greedy_resource_id ) {
        $self->_create_method($resource_id);
        $self->_create_method_response($resource_id);
        $self->_create_integration( $function_arn, $resource_id );
        $self->_create_integration_response($resource_id);
    }
    $self->_stage;

    return ();
}

sub api_url {
    my $self = shift;

    return
          'https://'
        . $self->rest_api_id
        . '.execute-api.'
        . $self->region
        . '.amazonaws.com/'
        . $self->stage_name . '/'
        . $self->name;
}

sub _stage {
    my $self = shift;

    $self->aws_do(
        'apigateway',
        'create-deployment',
        {
            'rest-api-id' => $self->rest_api_id,
            'stage-name'  => $self->stage_name,
        }
    );
}

sub _create_method {
    my $self        = shift;
    my $resource_id = shift;

    my @identifiers = (
        'rest-api-id' => $self->rest_api_id,
        'resource-id' => $resource_id,
        'http-method' => 'ANY',
    );

    $self->debug('checking for existing method');

lib/AWS/Lambda/Quick/Upload.pm  view on Meta::CPAN

        {
            @identifiers,
            'authorization-type' => 'NONE',
        },
    );
    $self->debug('new method put');

    return ();
}

sub _create_method_response {
    my $self        = shift;
    my $resource_id = shift;

    my $identifiers = {
        'rest-api-id' => $self->rest_api_id,
        'resource-id' => $resource_id,
        'http-method' => 'ANY',
        'status-code' => 200,
    };

lib/AWS/Lambda/Quick/Upload.pm  view on Meta::CPAN

    $self->aws_do(
        'apigateway',
        'put-method-response',
        $identifiers,
    );
    $self->debug('new method response put');

    return ();
}

sub _create_integration {
    my $self         = shift;
    my $function_arn = shift;
    my $resource_id  = shift;

    my $identifiers = {
        'rest-api-id' => $self->rest_api_id,
        'resource-id' => $resource_id,
        'http-method' => 'ANY',
    };

lib/AWS/Lambda/Quick/Upload.pm  view on Meta::CPAN

            'integration-http-method' => 'POST',
            'credential'              => $self->_role_arn,
            uri                       => $uri,
        }
    );
    $self->debug('new integration put');

    return ();
}

sub _create_integration_response {
    my $self        = shift;
    my $resource_id = shift;

    my $identifiers = {
        'rest-api-id' => $self->rest_api_id,
        'resource-id' => $resource_id,
        'http-method' => 'ANY',
        'status-code' => 200,
    };

lib/AWS/Lambda/Quick/Upload.pm  view on Meta::CPAN

        {
            %{$identifiers},
            'selection-pattern' => q{},
        }
    );
    $self->debug('new integration put');

    return ();
}

sub _upload_function {
    my $self = shift;

    my $update_type = $self->update_type;
    my $region      = $self->region;

    # compute the arn based on the list in the AWS::Lambda 0.0.11
    # documentation
    my $v      = $region eq 'me-south-1' ? 3 : 5;
    my $layers = [
        "arn:aws:lambda:$region:445285296882:layer:perl-5-30-runtime:$v",

lib/AWS/Lambda/Quick/Upload.pm  view on Meta::CPAN

            'layers'        => $layers,
            'timeout'       => $self->timeout,
            'memory-size'   => $self->memory_size,
        }
    );
    $self->debug('function congifuration updated');
    return $result->{FunctionArn};
}

# just like $self->aws->$method but throws exception on error
sub aws_do {
    my $self   = shift;
    my $method = shift;

    my $aws    = $self->aws;
    my $result = $aws->$method(@_);

    return $result if defined $result;

    # uh oh, something went wrong, throw exception

    ## no critic (ProhibitPackageVars)
    my $code    = $AWS::CLIWrapper::Error->{Code};
    my $message = $AWS::CLIWrapper::Error->{Message};

    die "AWS CLI failure when calling $method $_[0] '$code': $message";
}

sub encode_json($) {
    return JSON::PP->new->ascii->canonical(1)->allow_nonref(1)->encode(shift);
}

sub debug {
    my $self = shift;
    return unless $ENV{AWS_LAMBDA_QUICK_DEBUG};
    for (@_) {
        print STDERR "$_\n" or die "Can't write to fh: $!";
    }
    return ();
}

sub just_update_function_code {
    my $self = shift;

    $self->aws_do(
        'lambda',
        'update-function-code',
        {
            'function-name' => $self->name,
            'zip-file'      => $self->zip_file_blob,
        },
    );

t/00-report-prereqs.t  view on Meta::CPAN


# hide optional CPAN::Meta modules from prereq scanner
# and check if they are available
my $cpan_meta = "CPAN::Meta";
my $cpan_meta_pre = "CPAN::Meta::Prereqs";
my $HAS_CPAN_META = eval "require $cpan_meta; $cpan_meta->VERSION('2.120900')" && eval "require $cpan_meta_pre"; ## no critic

# Verify requirements?
my $DO_VERIFY_PREREQS = 1;

sub _max {
    my $max = shift;
    $max = ( $_ > $max ) ? $_ : $max for @_;
    return $max;
}

sub _merge_prereqs {
    my ($collector, $prereqs) = @_;

    # CPAN::Meta::Prereqs object
    if (ref $collector eq $cpan_meta_pre) {
        return $collector->with_merged_prereqs(
            CPAN::Meta::Prereqs->new( $prereqs )
        );
    }

    # Raw hashrefs

t/02zip.t  view on Meta::CPAN

is( $z->contents('handler.pl'), <<'PERL', 'hander.pl was stored ok' );
BEGIN{$INC{'AWS/Lambda/Quick.pm'}=1} use AWS::Lambda::Quick (
    name => 'whatever',
    extra_files => 'lib',
);

use lib qw(lib);
use Greeting;
use JSON::PP qw( encode_json );

sub handler {
    my $data = shift;

    return {
        statusCode => 200,
        headers => {
            'Content-Type' => 'text/plain',
        },
        body => Greeting->greeting( $data->{queryStringParameters}{who} ),
    };
}
1;
PERL

is( $z->contents('lib/Greeting.pm'), <<'PERL', 'Greeting.pm was stored ok' );
package Greeting;
sub greeting {
    my $class = shift;
    my $name  = shift;

    return "Hello, $name";
}
1;
PERL

done_testing;

t/lib/TestHelper/CreateTestFiles.pm  view on Meta::CPAN


use strict;
use warnings;

use Test::TempDir::Tiny qw( tempdir );
use Path::Tiny qw( path );

use base qw(Exporter);
our @EXPORT_OK = qw( populated_tempdir );

sub populated_tempdir {

    # make a clean temp dir
    my $tempdir = path( tempdir() );
    $tempdir->remove_tree if -e $tempdir;
    $tempdir->mkpath;

    # create the temp files
    my $dir = path( $tempdir, 'src' );
    $dir->mkpath;

t/lib/TestHelper/CreateTestFiles.pm  view on Meta::CPAN

    path( $dir, 'handler.pl' )->spew(<<'PERL');
use AWS::Lambda::Quick (
    name => 'whatever',
    extra_files => 'lib',
);

use lib qw(lib);
use Greeting;
use JSON::PP qw( encode_json );

sub handler {
    my $data = shift;

    return {
        statusCode => 200,
        headers => {
            'Content-Type' => 'text/plain',
        },
        body => Greeting->greeting( $data->{queryStringParameters}{who} ),
    };
}
1;
PERL

    # an example library
    path( $dir, 'lib' )->mkpath;
    path( $dir, 'lib', 'Greeting.pm' )->spew(<<'PERL');
package Greeting;
sub greeting {
    my $class = shift;
    my $name  = shift;

    return "Hello, $name";
}
1;
PERL

    return $tempdir;
}

xt/release/cpan-changes.t  view on Meta::CPAN

use strict;
use warnings;

# this test was generated with Dist::Zilla::Plugin::Test::CPAN::Changes 0.012

use Test::More 0.96 tests => 1;
use Test::CPAN::Changes;
subtest 'changes_ok' => sub {
    changes_file_ok('Changes');
};



( run in 0.335 second using v1.01-cache-2.11-cpan-a5abf4f5562 )