File Coverage

blib/lib/AWS/Lambda/Quick/Upload.pm
Criterion Covered Total %
statement 9 121 7.4
branch 0 24 0.0
condition n/a
subroutine 3 15 20.0
pod 0 6 0.0
total 12 166 7.2


line stmt bran cond sub pod time code
1             package AWS::Lambda::Quick::Upload;
2 1     1   8 use Mo qw( default required );
  1         2  
  1         6  
3              
4             our $VERSION = '1.0002';
5              
6 1     1   897 use AWS::CLIWrapper;
  1         58219  
  1         38  
7 1     1   745 use JSON::PP ();
  1         13951  
  1         2364  
8              
9             ### required attributes
10              
11             has zip_filename => required => 1;
12             has name => required => 1;
13              
14             ### optional attributes wrt the lambda function itself
15              
16             has extra_layers => default => [];
17             has region => default => 'us-east-1';
18             has memory_size => default => 128; # this is the AWS default
19             has timeout => default => 3; # this is the AWS default
20             has description => default => 'A Perl AWS::Lambda::Quick Lambda function.';
21             has stage_name => default => 'quick';
22              
23             ### lambda function computed attributes
24              
25             has aws => sub {
26             my $self = shift;
27              
28             return AWS::CLIWrapper->new(
29             region => $self->region,
30             );
31             };
32              
33             has zip_file_blob => sub { 'fileb://' . shift->zip_filename };
34              
35             # should we create the function from scratch or just update it?
36             # by default we interogate the api to see if it exists already
37             has update_type => sub {
38             my $self = shift;
39             my $aws = $self->aws;
40              
41             my $result = $aws->lambda(
42             'get-function',
43             {
44             'function-name' => $self->name,
45             }
46             );
47              
48             return $result ? 'update-function' : 'create-function';
49             };
50              
51             ### role attributes
52              
53             has role => default => 'perl-aws-lambda-quick';
54             has _role_arn => sub {
55             my $self = shift;
56              
57             # if whatever we were passed in role was an actual ARN then we
58             # can just use that without any further lookups
59             if ( $self->role
60             =~ /^arn:(aws[a-zA-Z-]*)?:iam::\d{12}:role\/?[a-zA-Z_0-9+=,.@\-_\/]+$/
61             ) {
62             $self->debug('using passed role arn');
63             return $self->role;
64             }
65              
66             $self->debug('searching for existing role');
67             my $aws = $self->aws;
68             my $result = $aws->iam(
69             'get-role',
70             {
71             'role-name' => $self->role,
72             }
73             );
74             if ($result) {
75             $self->debug('found existing role');
76             return $result->{Role}{Arn};
77             }
78              
79             $self->debug('creating new role');
80             $result = $self->aws_do(
81             'iam',
82             'create-role',
83             {
84             'role-name' => $self->role,
85             'description' =>
86             'Role for lambda functions created by AWS::Lambda::Quick. See https://metacpan.org/pod/AWS::Lambda::Quick for more info.',
87             'assume-role-policy-document' => <<'JSON',
88             {
89             "Version": "2012-10-17",
90             "Statement": [
91             {
92             "Action": "sts:AssumeRole",
93             "Effect": "Allow",
94             "Principal": {
95             "Service": [
96             "lambda.amazonaws.com",
97             "apigateway.amazonaws.com"
98             ]
99             }
100             }
101             ]
102             }
103             JSON
104             }
105             );
106             $self->debug('new role created');
107             $self->debug('attaching permissions to role');
108             $self->aws_do(
109             'iam',
110             'attach-role-policy',
111             {
112             'policy-arn' =>
113             'arn:aws:iam::aws:policy/service-role/AWSLambdaRole',
114             'role-name' => $self->role,
115             }
116             );
117             $self->aws_do(
118             'iam',
119             'attach-role-policy',
120             {
121             'policy-arn' =>
122             'arn:aws:iam::aws:policy/CloudWatchLogsFullAccess',
123             'role-name' => $self->role,
124             }
125             );
126             $self->debug('permissions attached to role');
127             return $result->{Role}{Arn};
128             };
129              
130             ### rest api attributes
131              
132             has rest_api => default => 'perl-aws-lambda-quick';
133             has rest_api_id => sub {
134             my $self = shift;
135              
136             # search existing apis
137             $self->debug('searching for existing rest api');
138             my $result = $self->aws_do(
139             'apigateway',
140             'get-rest-apis',
141             );
142             for ( @{ $result->{items} } ) {
143             next unless $_->{name} eq $self->rest_api;
144             $self->debug('found existing existing rest api');
145             return $_->{id};
146             }
147              
148             # couldn't find it. Create a new one
149             $self->debug('creating new rest api');
150             $result = $self->aws_do(
151             'apigateway',
152             'create-rest-api',
153             {
154             name => $self->rest_api,
155             description =>
156             'Created by AWS::Lambda::Quick. See https://metacpan.org/pod/AWS::Lambda::Quick for more info.',
157             },
158             );
159             $self->debug('created new rest api');
160             return $result->{id};
161             };
162              
163             has resource_id => sub {
164             my $self = shift;
165              
166             # TODO: We shold probably make this configurable, right?
167             my $path = '/' . $self->name;
168              
169             # search existing resources
170             $self->debug('searching of existing resource');
171             my $result = $self->aws_do(
172             'apigateway',
173             'get-resources',
174             {
175             'rest-api-id' => $self->rest_api_id,
176             }
177             );
178             for ( @{ $result->{items} } ) {
179             next unless $_->{path} eq $path;
180             $self->debug('found exiting resource');
181             return $_->{id};
182             }
183              
184             # couldn't find it. Create a new one
185             $self->debug('creating new resource');
186             my $parent_id;
187             for ( @{ $result->{items} } ) {
188             if ( $_->{path} eq '/' ) {
189             $parent_id = $_->{id};
190             last;
191             }
192             }
193             unless ($parent_id) {
194             die q{Can't find '/' resource to create a new resource from!};
195             }
196             $result = $self->aws_do(
197             'apigateway',
198             'create-resource',
199             {
200             'rest-api-id' => $self->rest_api_id,
201             'parent-id' => $parent_id,
202             'path-part' => $self->name,
203             },
204             );
205             $self->debug('created new resource');
206             return $result->{id};
207             };
208              
209             has greedy_resource_id => sub {
210             my $self = shift;
211              
212             my $path = '/' . $self->name . '/{proxy+}';
213              
214             # search existing resources
215             $self->debug('searching of existing greedy resource');
216             my $result = $self->aws_do(
217             'apigateway',
218             'get-resources',
219             {
220             'rest-api-id' => $self->rest_api_id,
221             }
222             );
223             for ( @{ $result->{items} } ) {
224             next unless $_->{path} eq $path;
225             $self->debug('found exiting resource');
226             return $_->{id};
227             }
228              
229             # couldn't find it. Create a new one
230             $self->debug('creating new greedy resource');
231             $result = $self->aws_do(
232             'apigateway',
233             'create-resource',
234             {
235             'rest-api-id' => $self->rest_api_id,
236             'parent-id' => $self->resource_id,
237             'path-part' => '{proxy+}',
238             },
239             );
240             $self->debug('created new greedy resource');
241             return $result->{id};
242             };
243              
244             ### methods
245              
246             sub upload {
247 0     0 0   my $self = shift;
248              
249 0           my $function_arn = $self->_upload_function;
250              
251 0           for my $resource_id ( $self->resource_id, $self->greedy_resource_id ) {
252 0           $self->_create_method($resource_id);
253 0           $self->_create_method_response($resource_id);
254 0           $self->_create_integration( $function_arn, $resource_id );
255 0           $self->_create_integration_response($resource_id);
256             }
257 0           $self->_stage;
258              
259 0           return ();
260             }
261              
262             sub api_url {
263 0     0 0   my $self = shift;
264              
265             return
266 0           'https://'
267             . $self->rest_api_id
268             . '.execute-api.'
269             . $self->region
270             . '.amazonaws.com/'
271             . $self->stage_name . '/'
272             . $self->name;
273             }
274              
275             sub _stage {
276 0     0     my $self = shift;
277              
278 0           $self->aws_do(
279             'apigateway',
280             'create-deployment',
281             {
282             'rest-api-id' => $self->rest_api_id,
283             'stage-name' => $self->stage_name,
284             }
285             );
286             }
287              
288             sub _create_method {
289 0     0     my $self = shift;
290 0           my $resource_id = shift;
291              
292 0           my @identifiers = (
293             'rest-api-id' => $self->rest_api_id,
294             'resource-id' => $resource_id,
295             'http-method' => 'ANY',
296             );
297              
298 0           $self->debug('checking for existing method');
299              
300             # get the current method
301 0           my $result = $self->aws->apigateway(
302             'get-method', {@identifiers},
303             );
304              
305 0 0         if ($result) {
306 0           $self->debug('found existing method');
307 0           return ();
308             }
309              
310 0           $self->debug('putting new method');
311 0           $self->aws_do(
312             'apigateway',
313             'put-method',
314             {
315             @identifiers,
316             'authorization-type' => 'NONE',
317             },
318             );
319 0           $self->debug('new method put');
320              
321 0           return ();
322             }
323              
324             sub _create_method_response {
325 0     0     my $self = shift;
326 0           my $resource_id = shift;
327              
328 0           my $identifiers = {
329             'rest-api-id' => $self->rest_api_id,
330             'resource-id' => $resource_id,
331             'http-method' => 'ANY',
332             'status-code' => 200,
333             };
334              
335 0           $self->debug('checking for existing method response');
336              
337             # get the current method response
338 0           my $result = $self->aws->apigateway(
339             'get-method-response', $identifiers,
340             );
341 0 0         if ($result) {
342 0           $self->debug('found existing method response');
343 0           return ();
344             }
345              
346 0           $self->debug('putting new method response');
347 0           $self->aws_do(
348             'apigateway',
349             'put-method-response',
350             $identifiers,
351             );
352 0           $self->debug('new method response put');
353              
354 0           return ();
355             }
356              
357             sub _create_integration {
358 0     0     my $self = shift;
359 0           my $function_arn = shift;
360 0           my $resource_id = shift;
361              
362 0           my $identifiers = {
363             'rest-api-id' => $self->rest_api_id,
364             'resource-id' => $resource_id,
365             'http-method' => 'ANY',
366             };
367              
368             # according the the documentation at https://docs.aws.amazon.com/cli/latest/reference/apigateway/put-integration.html
369             # the uri has the form arn:aws:apigateway:{region}:{subdomain.service|service}:path|action/{service_api}
370             # "lambda:path/2015-03-31/functions" is the {subdomain.service|service}:path|action for lambda functions
371 0           my $uri
372 0           = "arn:aws:apigateway:@{[ $self->region ]}:lambda:path/2015-03-31/functions/$function_arn/invocations";
373              
374 0           $self->debug('checking for existing integration');
375              
376             # get the current method response
377 0           my $result = $self->aws->apigateway(
378             'get-integration', $identifiers,
379             );
380 0 0         if ($result) {
381 0           $self->debug('found existing integration');
382 0           return ();
383             }
384              
385 0           $self->debug('putting new integration');
386             $self->aws_do(
387             'apigateway',
388             'put-integration',
389             {
390 0           %{$identifiers},
  0            
391             type => 'AWS_PROXY',
392             'integration-http-method' => 'POST',
393             'credential' => $self->_role_arn,
394             uri => $uri,
395             }
396             );
397 0           $self->debug('new integration put');
398              
399 0           return ();
400             }
401              
402             sub _create_integration_response {
403 0     0     my $self = shift;
404 0           my $resource_id = shift;
405              
406 0           my $identifiers = {
407             'rest-api-id' => $self->rest_api_id,
408             'resource-id' => $resource_id,
409             'http-method' => 'ANY',
410             'status-code' => 200,
411             };
412              
413 0           $self->debug('checking for existing integration response');
414              
415             # get the current method response
416 0           my $result = $self->aws->apigateway(
417             'get-integration-response', $identifiers,
418             );
419 0 0         if ($result) {
420 0           $self->debug('found existing integration response');
421 0           return ();
422             }
423              
424 0           $self->debug('putting new integration');
425             $self->aws_do(
426             'apigateway',
427             'put-integration-response',
428             {
429 0           %{$identifiers},
  0            
430             'selection-pattern' => q{},
431             }
432             );
433 0           $self->debug('new integration put');
434              
435 0           return ();
436             }
437              
438             sub _upload_function {
439 0     0     my $self = shift;
440              
441 0           my $update_type = $self->update_type;
442 0           my $region = $self->region;
443              
444             # compute the arn based on the list in the AWS::Lambda 0.0.11
445             # documentation
446 0 0         my $v = $region eq 'me-south-1' ? 3 : 5;
447 0           my $layers = [
448             "arn:aws:lambda:$region:445285296882:layer:perl-5-30-runtime:$v",
449             ];
450              
451 0           for my $layer ( @{ $self->extra_layers } ) {
  0            
452 0 0         if ( $layer
453             =~ /(arn:[a-zA-Z0-9-]+:lambda:[a-zA-Z0-9-]+:\d{12}:layer:[a-zA-Z0-9-_]+)/aa
454             ) {
455 0           push @{$layers}, $layer;
  0            
456 0           next;
457             }
458              
459 0 0         if ( $layer eq 'paws' ) {
460              
461             # compute the arn based on the list in the AWS::Lambda 0.0.11
462             # documentation
463 0 0         my $pv = $region eq 'me-south-1' ? 3 : 4;
464 0           push @{$layers},
  0            
465             "arn:aws:lambda:$region:445285296882:layer:perl-5-30-paws:$pv";
466 0           next;
467             }
468              
469 0           die "Layer '$layer' is neither a known named layer nor a layer arn";
470             }
471              
472 0 0         if ( $update_type eq 'create-function' ) {
473 0           $self->debug('creating new function');
474 0           my $result = $self->aws_do(
475             'lambda',
476             'create-function',
477             {
478             'function-name' => $self->name,
479             'role' => $self->_role_arn,
480             'region' => $region,
481             'runtime' => 'provided',
482             'zip-file' => $self->zip_file_blob,
483             'handler' => 'handler.handler',
484             'layers' => $layers,
485             'timeout' => $self->timeout,
486             'memory-size' => $self->memory_size,
487             }
488             );
489 0           $self->debug('new function created');
490 0           return $result->{FunctionArn};
491             }
492              
493 0           $self->debug('updating function code');
494 0           my $result = $self->aws_do(
495             'lambda',
496             'update-function-code',
497             {
498             'function-name' => $self->name,
499             'zip-file' => $self->zip_file_blob,
500             }
501             );
502 0           $self->debug('function code updated');
503 0           $self->debug('updating function configuration');
504 0           $self->aws_do(
505             'lambda',
506             'update-function-configuration',
507             {
508             'function-name' => $self->name,
509             'role' => $self->_role_arn,
510             'region' => $region,
511             'runtime' => 'provided',
512             'handler' => 'handler.handler',
513             'layers' => $layers,
514             'timeout' => $self->timeout,
515             'memory-size' => $self->memory_size,
516             }
517             );
518 0           $self->debug('function congifuration updated');
519 0           return $result->{FunctionArn};
520             }
521              
522             # just like $self->aws->$method but throws exception on error
523             sub aws_do {
524 0     0 0   my $self = shift;
525 0           my $method = shift;
526              
527 0           my $aws = $self->aws;
528 0           my $result = $aws->$method(@_);
529              
530 0 0         return $result if defined $result;
531              
532             # uh oh, something went wrong, throw exception
533              
534             ## no critic (ProhibitPackageVars)
535 0           my $code = $AWS::CLIWrapper::Error->{Code};
536 0           my $message = $AWS::CLIWrapper::Error->{Message};
537              
538 0           die "AWS CLI failure when calling $method $_[0] '$code': $message";
539             }
540              
541             sub encode_json($) {
542 0     0 0   return JSON::PP->new->ascii->canonical(1)->allow_nonref(1)->encode(shift);
543             }
544              
545             sub debug {
546 0     0 0   my $self = shift;
547 0 0         return unless $ENV{AWS_LAMBDA_QUICK_DEBUG};
548 0           for (@_) {
549 0 0         print STDERR "$_\n" or die "Can't write to fh: $!";
550             }
551 0           return ();
552             }
553              
554             sub just_update_function_code {
555 0     0 0   my $self = shift;
556              
557 0           $self->aws_do(
558             'lambda',
559             'update-function-code',
560             {
561             'function-name' => $self->name,
562             'zip-file' => $self->zip_file_blob,
563             },
564             );
565              
566 0           return ();
567             }
568              
569             1;
570              
571             __END__
572              
573             =head1 NAME
574              
575             AWS::Lambda::Quick::Upload - upload for AWS::Lambda::Quick
576              
577             =head1 DESCRIPTION
578              
579             No user servicable parts. See L<AWS::Lambda::Quick> for usage.
580              
581             =head1 AUTHOR
582              
583             Written by Mark Fowler B<mark@twoshortplanks.com>
584              
585             Copyright Mark Fowler 2019.
586              
587             This program is free software; you can redistribute it and/or modify
588             it under the same terms as Perl itself.
589              
590             =head1 SEE ALSO
591              
592             L<AWS::Lambda::Quick>
593              
594             =cut