File Coverage

blib/lib/AWS/Lambda/Quick/Upload.pm
Criterion Covered Total %
statement 9 102 8.8
branch 0 16 0.0
condition n/a
subroutine 3 15 20.0
pod 0 6 0.0
total 12 139 8.6


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