-
Notifications
You must be signed in to change notification settings - Fork 5.5k
/
Copy pathboto_apigateway.py
2084 lines (1735 loc) · 87 KB
/
boto_apigateway.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# -*- coding: utf-8 -*-
'''
Manage Apigateway Rest APIs
===========================
.. versionadded:: 2016.11.0
:depends:
- boto >= 2.8.0
- boto3 >= 1.2.1
- botocore >= 1.4.49
Create and destroy rest apis depending on a swagger version 2 definition file.
Be aware that this interacts with Amazon's services, and so may incur charges.
This module uses ``boto3``, which can be installed via package, or pip.
This module accepts explicit vpc credentials but can also utilize
IAM roles assigned to the instance through Instance Profiles. Dynamic
credentials are then automatically obtained from AWS API and no further
configuration is necessary. More information available `here
<http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html>`_.
If IAM roles are not used you need to specify them either in a pillar file or
in the minion's config file:
.. code-block:: yaml
vpc.keyid: GKTADJGHEIQSXMKKRBJ08H
vpc.key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
It's also possible to specify ``key``, ``keyid`` and ``region`` via a profile,
either passed in as a dict, or as a string to pull from pillars or minion
config:
.. code-block:: yaml
myprofile:
keyid: GKTADJGHEIQSXMKKRBJ08H
key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
region: us-east-1
.. code-block:: yaml
Ensure Apigateway API exists:
boto_apigateway.present:
- name: myfunction
- region: us-east-1
- keyid: GKTADJGHEIQSXMKKRBJ08H
- key: askdjghsdfjkghWupUjasdflkdfklgjsdfjajkghs
'''
# Import Python Libs
from __future__ import absolute_import, print_function, unicode_literals
import hashlib
import logging
import os
import re
# Import Salt Libs
import salt.utils.files
import salt.utils.json
import salt.utils.yaml
# Import 3rd-party libs
from salt.ext import six
log = logging.getLogger(__name__)
def __virtual__():
'''
Only load if boto is available.
'''
return 'boto_apigateway' if 'boto_apigateway.describe_apis' in __salt__ else False
def present(name, api_name, swagger_file, stage_name, api_key_required,
lambda_integration_role, lambda_region=None, stage_variables=None,
region=None, key=None, keyid=None, profile=None,
lambda_funcname_format='{stage}_{api}_{resource}_{method}',
authorization_type='NONE', error_response_template=None, response_template=None):
'''
Ensure the spcified api_name with the corresponding swaggerfile is deployed to the
given stage_name in AWS ApiGateway.
this state currently only supports ApiGateway integration with AWS Lambda, and CORS support is
handled through a Mock integration.
There may be multiple deployments for the API object, each deployment is tagged with a description
(i.e. unique label) in pretty printed json format consisting of the following key/values.
.. code-block:: text
{
"api_name": api_name,
"swagger_file": basename_of_swagger_file
"swagger_file_md5sum": md5sum_of_swagger_file,
"swagger_info_object": info_object_content_in_swagger_file
}
Please note that the name of the lambda function to be integrated will be derived
via the provided lambda_funcname_format parameters:
- the default lambda_funcname_format is a string with the following
substitutable keys: "{stage}_{api}_{resource}_{method}". The user can
choose to reorder the known keys.
- the stage key corresponds to the stage_name passed in.
- the api key corresponds to the api_name passed in.
- the resource corresponds to the resource path defined in the passed swagger file.
- the method corresponds to the method for a resource path defined in the passed swagger file.
For the default lambda_funcname_format, given the following input:
.. code-block:: python
api_name = ' Test Service'
stage_name = 'alpha'
basePath = '/api'
path = '/a/{b}/c'
method = 'POST'
We will end up with the following Lambda Function Name that will be looked
up: 'test_service_alpha_a_b_c_post'
The canconicalization of these input parameters is done in the following order:
1. lambda_funcname_format is formatted with the input parameters as passed,
2. resulting string is stripped for leading/trailing spaces,
3. path parameter's curly braces are removed from the resource path,
4. consecutive spaces and forward slashes in the paths are replaced with '_'
5. consecutive '_' are replaced with '_'
Please note that for error response handling, the swagger file must have an error response model
with the following schema. The lambda functions should throw exceptions for any non successful responses.
An optional pattern field can be specified in errorMessage field to aid the response mapping from Lambda
to the proper error return status codes.
.. code-block:: yaml
Error:
type: object
properties:
stackTrace:
type: array
items:
type: array
items:
type: string
description: call stack
errorType:
type: string
description: error type
errorMessage:
type: string
description: |
Error message, will be matched based on pattern.
If no pattern is specified, the default pattern used for response mapping will be +*.
name
The name of the state definition
api_name
The name of the rest api that we want to ensure exists in AWS API Gateway
swagger_file
Name of the location of the swagger rest api definition file in YAML format.
stage_name
Name of the stage we want to be associated with the given api_name and swagger_file
definition
api_key_required
True or False - whether the API Key is required to call API methods
lambda_integration_role
The name or ARN of the IAM role that the AWS ApiGateway assumes when it
executes your lambda function to handle incoming requests
lambda_region
The region where we expect to find the lambda functions. This is used to
determine the region where we should look for the Lambda Function for
integration purposes. The region determination is based on the following
priority:
1. lambda_region as passed in (is not None)
2. if lambda_region is None, use the region as if a boto_lambda
function were executed without explicitly specifying lambda region.
3. if region determined in (2) is different than the region used by
boto_apigateway functions, a final lookup will be attempted using
the boto_apigateway region.
stage_variables
A dict with variables and their values, or a pillar key (string) that
contains a dict with variables and their values.
key and values in the dict must be strings. {'string': 'string'}
region
Region to connect to.
key
Secret key to be used.
keyid
Access key to be used.
profile
A dict with region, key and keyid, or a pillar key (string) that
contains a dict with region, key and keyid.
lambda_funcname_format
Please review the earlier example for the usage. The only substituable keys in the funcname
format are {stage}, {api}, {resource}, {method}.
Any other keys or positional subsitution parameters will be flagged as an invalid input.
authorization_type
This field can be either 'NONE', or 'AWS_IAM'. This will be applied to all methods in the given
swagger spec file. Default is set to 'NONE'
error_response_template
String value that defines the response template mapping that should be applied in cases error occurs.
Refer to AWS documentation for details: http://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-mapping-template-reference.html
If set to None, the following default value is used:
.. code-block:: text
'#set($inputRoot = $input.path(\'$\'))\\n'
'{\\n'
' "errorMessage" : "$inputRoot.errorMessage",\\n'
' "errorType" : "$inputRoot.errorType",\\n'
' "stackTrace" : [\\n'
'#foreach($stackTrace in $inputRoot.stackTrace)\\n'
' [\\n'
'#foreach($elem in $stackTrace)\\n'
' "$elem"\\n'
'#if($foreach.hasNext),#end\\n'
'#end\\n'
' ]\\n'
'#if($foreach.hasNext),#end\\n'
'#end\\n'
' ]\\n'
.. versionadded:: 2017.7.0
response_template
String value that defines the response template mapping applied in case
of success (including OPTIONS method) If set to None, empty ({})
template is assumed, which will transfer response from the lambda
function as is.
.. versionadded:: 2017.7.0
'''
ret = {'name': name,
'result': True,
'comment': '',
'changes': {}
}
try:
common_args = dict([('region', region),
('key', key),
('keyid', keyid),
('profile', profile)])
# try to open the swagger file and basic validation
swagger = _Swagger(api_name, stage_name,
lambda_funcname_format,
swagger_file,
error_response_template, response_template,
common_args)
# retrieve stage variables
stage_vars = _get_stage_variables(stage_variables)
# verify if api and stage already exists
ret = swagger.verify_api(ret)
if ret.get('publish'):
# there is a deployment label with signature matching the given api_name,
# swagger file name, swagger file md5 sum, and swagger file info object
# just reassociate the stage_name to the given deployment label.
if __opts__['test']:
ret['comment'] = ('[stage: {0}] will be reassociated to an already available '
'deployment that matched the given [api_name: {1}] '
'and [swagger_file: {2}].\n'
'Stage variables will be set '
'to {3}.'.format(stage_name, api_name, swagger_file, stage_vars))
ret['result'] = None
return ret
return swagger.publish_api(ret, stage_vars)
if ret.get('current'):
# already at desired state for the stage, swagger_file, and api_name
if __opts__['test']:
ret['comment'] = ('[stage: {0}] is already at desired state with an associated '
'deployment matching the given [api_name: {1}] '
'and [swagger_file: {2}].\n'
'Stage variables will be set '
'to {3}.'.format(stage_name, api_name, swagger_file, stage_vars))
ret['result'] = None
return swagger.overwrite_stage_variables(ret, stage_vars)
# there doesn't exist any previous deployments for the given swagger_file, we need
# to redeploy the content of the swagger file to the api, models, and resources object
# and finally create a new deployment and tie the stage_name to this new deployment
if __opts__['test']:
ret['comment'] = ('There is no deployment matching the given [api_name: {0}] '
'and [swagger_file: {1}]. A new deployment will be '
'created and the [stage_name: {2}] will then be associated '
'to the newly created deployment.\n'
'Stage variables will be set '
'to {3}.'.format(api_name, swagger_file, stage_name, stage_vars))
ret['result'] = None
return ret
ret = swagger.deploy_api(ret)
if ret.get('abort'):
return ret
ret = swagger.deploy_models(ret)
if ret.get('abort'):
return ret
ret = swagger.deploy_resources(ret,
api_key_required=api_key_required,
lambda_integration_role=lambda_integration_role,
lambda_region=lambda_region,
authorization_type=authorization_type)
if ret.get('abort'):
return ret
ret = swagger.publish_api(ret, stage_vars)
except (ValueError, IOError) as e:
ret['result'] = False
ret['comment'] = '{0}'.format(e.args)
return ret
def _get_stage_variables(stage_variables):
'''
Helper function to retrieve stage variables from pillars/options, if the
input is a string
'''
ret = dict()
if stage_variables is None:
return ret
if isinstance(stage_variables, six.string_types):
if stage_variables in __opts__:
ret = __opts__[stage_variables]
master_opts = __pillar__.get('master', {})
if stage_variables in master_opts:
ret = master_opts[stage_variables]
if stage_variables in __pillar__:
ret = __pillar__[stage_variables]
elif isinstance(stage_variables, dict):
ret = stage_variables
if not isinstance(ret, dict):
ret = dict()
return ret
def absent(name, api_name, stage_name, nuke_api=False, region=None, key=None, keyid=None, profile=None):
'''
Ensure the stage_name associated with the given api_name deployed by boto_apigateway's
present state is removed. If the currently associated deployment to the given stage_name has
no other stages associated with it, the deployment will also be removed.
name
Name of the swagger file in YAML format
api_name
Name of the rest api on AWS ApiGateway to ensure is absent.
stage_name
Name of the stage to be removed irrespective of the swagger file content.
If the current deployment associated with the stage_name has no other stages associated
with it, the deployment will also be removed.
nuke_api
If True, removes the API itself only if there are no other stages associated with any other
deployments once the given stage_name is removed.
region
Region to connect to.
key
Secret key to be used.
keyid
Access key to be used.
profile
A dict with region, key and keyid, or a pillar key (string) that
contains a dict with region, key and keyid.
'''
ret = {'name': name,
'result': True,
'comment': '',
'changes': {}
}
try:
common_args = dict([('region', region),
('key', key),
('keyid', keyid),
('profile', profile)])
swagger = _Swagger(api_name, stage_name, '', None, None, None, common_args)
if not swagger.restApiId:
ret['comment'] = '[Rest API: {0}] does not exist.'.format(api_name)
return ret
if __opts__['test']:
if nuke_api:
ret['comment'] = ('[stage: {0}] will be deleted, if there are no other '
'active stages, the [api: {1} will also be '
'deleted.'.format(stage_name, api_name))
else:
ret['comment'] = ('[stage: {0}] will be deleted.'.format(stage_name))
ret['result'] = None
return ret
ret = swagger.delete_stage(ret)
if ret.get('abort'):
return ret
if nuke_api and swagger.no_more_deployments_remain():
ret = swagger.delete_api(ret)
except (ValueError, IOError) as e:
ret['result'] = False
ret['comment'] = '{0}'.format(e.args)
return ret
# Helper Swagger Class for swagger version 2.0 API specification
def _gen_md5_filehash(fname, *args):
'''
helper function to generate a md5 hash of the swagger definition file
any extra argument passed to the function is converted to a string
and participates in the hash calculation
'''
_hash = hashlib.md5()
with salt.utils.files.fopen(fname, 'rb') as f:
for chunk in iter(lambda: f.read(4096), b''):
_hash.update(chunk)
for extra_arg in args:
_hash.update(six.b(str(extra_arg)))
return _hash.hexdigest()
def _dict_to_json_pretty(d, sort_keys=True):
'''
helper function to generate pretty printed json output
'''
return salt.utils.json.dumps(d, indent=4, separators=(',', ': '), sort_keys=sort_keys)
# Heuristic on whether or not the property name loosely matches given set of 'interesting' factors
# If you are interested in IDs for example, 'id', 'blah_id', 'blahId' would all match
def _name_matches(name, matches):
'''
Helper function to see if given name has any of the patterns in given matches
'''
for m in matches:
if name.endswith(m):
return True
if name.lower().endswith('_' + m.lower()):
return True
if name.lower() == m.lower():
return True
return False
def _object_reducer(o, names=('id', 'name', 'path', 'httpMethod',
'statusCode', 'Created', 'Deleted',
'Updated', 'Flushed', 'Associated', 'Disassociated')):
'''
Helper function to reduce the amount of information that will be kept in the change log
for API GW related return values
'''
result = {}
if isinstance(o, dict):
for k, v in six.iteritems(o):
if isinstance(v, dict):
reduced = v if k == 'variables' else _object_reducer(v, names)
if reduced or _name_matches(k, names):
result[k] = reduced
elif isinstance(v, list):
newlist = []
for val in v:
reduced = _object_reducer(val, names)
if reduced or _name_matches(k, names):
newlist.append(reduced)
if newlist:
result[k] = newlist
else:
if _name_matches(k, names):
result[k] = v
return result
def _log_changes(ret, changekey, changevalue):
'''
For logging create/update/delete operations to AWS ApiGateway
'''
cl = ret['changes'].get('new', [])
cl.append({changekey: _object_reducer(changevalue)})
ret['changes']['new'] = cl
return ret
def _log_error_and_abort(ret, obj):
'''
helper function to update errors in the return structure
'''
ret['result'] = False
ret['abort'] = True
if 'error' in obj:
ret['comment'] = '{0}'.format(obj.get('error'))
return ret
class _Swagger(object):
'''
this is a helper class that holds the swagger definition file and the associated logic
related to how to interpret the file and apply it to AWS Api Gateway.
The main interface to the outside world is in deploy_api, deploy_models, and deploy_resources
methods.
'''
SWAGGER_OBJ_V2_FIELDS = ('swagger', 'info', 'host', 'basePath', 'schemes', 'consumes', 'produces',
'paths', 'definitions', 'parameters', 'responses', 'securityDefinitions',
'security', 'tags', 'externalDocs')
# SWAGGER OBJECT V2 Fields that are required by boto apigateway states.
SWAGGER_OBJ_V2_FIELDS_REQUIRED = ('swagger', 'info', 'basePath', 'schemes', 'paths', 'definitions')
# SWAGGER OPERATION NAMES
SWAGGER_OPERATION_NAMES = ('get', 'put', 'post', 'delete', 'options', 'head', 'patch')
SWAGGER_VERSIONS_SUPPORTED = ('2.0',)
# VENDOR SPECIFIC FIELD PATTERNS
VENDOR_EXT_PATTERN = re.compile('^x-')
# JSON_SCHEMA_REF
JSON_SCHEMA_DRAFT_4 = 'http://json-schema.org/draft-04/schema#'
# AWS integration templates for normal and options methods
REQUEST_TEMPLATE = {'application/json': '#set($inputRoot = $input.path(\'$\'))\n'
'{\n'
'"header_params" : {\n'
'#set ($map = $input.params().header)\n'
'#foreach( $param in $map.entrySet() )\n'
'"$param.key" : "$param.value" #if( $foreach.hasNext ), #end\n'
'#end\n'
'},\n'
'"query_params" : {\n'
'#set ($map = $input.params().querystring)\n'
'#foreach( $param in $map.entrySet() )\n'
'"$param.key" : "$param.value" #if( $foreach.hasNext ), #end\n'
'#end\n'
'},\n'
'"path_params" : {\n'
'#set ($map = $input.params().path)\n'
'#foreach( $param in $map.entrySet() )\n'
'"$param.key" : "$param.value" #if( $foreach.hasNext ), #end\n'
'#end\n'
'},\n'
'"apigw_context" : {\n'
'"apiId": "$context.apiId",\n'
'"httpMethod": "$context.httpMethod",\n'
'"requestId": "$context.requestId",\n'
'"resourceId": "$context.resourceId",\n'
'"resourcePath": "$context.resourcePath",\n'
'"stage": "$context.stage",\n'
'"identity": {\n'
' "user":"$context.identity.user",\n'
' "userArn":"$context.identity.userArn",\n'
' "userAgent":"$context.identity.userAgent",\n'
' "sourceIp":"$context.identity.sourceIp",\n'
' "cognitoIdentityId":"$context.identity.cognitoIdentityId",\n'
' "cognitoIdentityPoolId":"$context.identity.cognitoIdentityPoolId",\n'
' "cognitoAuthenticationType":"$context.identity.cognitoAuthenticationType",\n'
' "cognitoAuthenticationProvider":["$util.escapeJavaScript($context.identity.cognitoAuthenticationProvider)"],\n'
' "caller":"$context.identity.caller",\n'
' "apiKey":"$context.identity.apiKey",\n'
' "accountId":"$context.identity.accountId"\n'
'}\n'
'},\n'
'"body_params" : $input.json(\'$\'),\n'
'"stage_variables": {\n'
'#foreach($variable in $stageVariables.keySet())\n'
'"$variable": "$util.escapeJavaScript($stageVariables.get($variable))"\n'
'#if($foreach.hasNext), #end\n'
'#end\n'
'}\n'
'}'}
REQUEST_OPTION_TEMPLATE = {'application/json': '{"statusCode": 200}'}
# AWS integration response template mapping to convert stackTrace part or the error
# to a uniform format containing strings only. Swagger does not seem to allow defining
# an array of non-uniform types, to it is not possible to create error model to match
# exactly what comes out of lambda functions in case of error.
RESPONSE_TEMPLATE = {'application/json': '#set($inputRoot = $input.path(\'$\'))\n'
'{\n'
' "errorMessage" : "$inputRoot.errorMessage",\n'
' "errorType" : "$inputRoot.errorType",\n'
' "stackTrace" : [\n'
'#foreach($stackTrace in $inputRoot.stackTrace)\n'
' [\n'
'#foreach($elem in $stackTrace)\n'
' "$elem"\n'
'#if($foreach.hasNext),#end\n'
'#end\n'
' ]\n'
'#if($foreach.hasNext),#end\n'
'#end\n'
' ]\n'
'}'}
RESPONSE_OPTION_TEMPLATE = {}
# This string should not be modified, every API created by this state will carry the description
# below.
AWS_API_DESCRIPTION = _dict_to_json_pretty({"provisioned_by": "Salt boto_apigateway.present State",
"context": "See deployment or stage description"})
class SwaggerParameter(object):
'''
This is a helper class for the Swagger Parameter Object
'''
LOCATIONS = ('body', 'query', 'header', 'path')
def __init__(self, paramdict):
self._paramdict = paramdict
@property
def location(self):
'''
returns location in the swagger parameter object
'''
_location = self._paramdict.get('in')
if _location in _Swagger.SwaggerParameter.LOCATIONS:
return _location
raise ValueError('Unsupported parameter location: {0} in Parameter Object'.format(_location))
@property
def name(self):
'''
returns parameter name in the swagger parameter object
'''
_name = self._paramdict.get('name')
if _name:
if self.location == 'header':
return 'method.request.header.{0}'.format(_name)
elif self.location == 'query':
return 'method.request.querystring.{0}'.format(_name)
elif self.location == 'path':
return 'method.request.path.{0}'.format(_name)
return None
raise ValueError('Parameter must have a name: {0}'.format(_dict_to_json_pretty(self._paramdict)))
@property
def schema(self):
'''
returns the name of the schema given the reference in the swagger parameter object
'''
if self.location == 'body':
_schema = self._paramdict.get('schema')
if _schema:
if '$ref' in _schema:
schema_name = _schema.get('$ref').split('/')[-1]
return schema_name
raise ValueError(('Body parameter must have a JSON reference '
'to the schema definition due to Amazon API restrictions: {0}'.format(self.name)))
raise ValueError('Body parameter must have a schema: {0}'.format(self.name))
return None
class SwaggerMethodResponse(object):
'''
Helper class for Swagger Method Response Object
'''
def __init__(self, r):
self._r = r
@property
def schema(self):
'''
returns the name of the schema given the reference in the swagger method response object
'''
_schema = self._r.get('schema')
if _schema:
if '$ref' in _schema:
return _schema.get('$ref').split('/')[-1]
raise ValueError(('Method response must have a JSON reference '
'to the schema definition: {0}'.format(_schema)))
return None
@property
def headers(self):
'''
returns the headers dictionary in the method response object
'''
_headers = self._r.get('headers', {})
return _headers
def __init__(self, api_name, stage_name, lambda_funcname_format,
swagger_file_path, error_response_template, response_template, common_aws_args):
self._api_name = api_name
self._stage_name = stage_name
self._lambda_funcname_format = lambda_funcname_format
self._common_aws_args = common_aws_args
self._restApiId = ''
self._deploymentId = ''
self._error_response_template = error_response_template
self._response_template = response_template
if swagger_file_path is not None:
if os.path.exists(swagger_file_path) and os.path.isfile(swagger_file_path):
self._swagger_file = swagger_file_path
self._md5_filehash = _gen_md5_filehash(self._swagger_file,
error_response_template,
response_template)
with salt.utils.files.fopen(self._swagger_file, 'rb') as sf:
self._cfg = salt.utils.yaml.safe_load(sf)
self._swagger_version = ''
else:
raise IOError('Invalid swagger file path, {0}'.format(swagger_file_path))
self._validate_swagger_file()
self._validate_lambda_funcname_format()
self._resolve_api_id()
def _is_http_error_rescode(self, code):
'''
Helper function to determine if the passed code is in the 400~599 range of http error
codes
'''
return bool(re.match(r'^\s*[45]\d\d\s*$', code))
def _validate_error_response_model(self, paths, mods):
'''
Helper function to help validate the convention established in the swagger file on how
to handle response code mapping/integration
'''
for path, ops in paths:
for opname, opobj in six.iteritems(ops):
if opname not in _Swagger.SWAGGER_OPERATION_NAMES:
continue
if 'responses' not in opobj:
raise ValueError('missing mandatory responses field in path item object')
for rescode, resobj in six.iteritems(opobj.get('responses')):
if not self._is_http_error_rescode(str(rescode)): # future lint: disable=blacklisted-function
continue
# only check for response code from 400-599
if 'schema' not in resobj:
raise ValueError('missing schema field in path {0}, '
'op {1}, response {2}'.format(path, opname, rescode))
schemaobj = resobj.get('schema')
if '$ref' not in schemaobj:
raise ValueError('missing $ref field under schema in '
'path {0}, op {1}, response {2}'.format(path, opname, rescode))
schemaobjref = schemaobj.get('$ref', '/')
modelname = schemaobjref.split('/')[-1]
if modelname not in mods:
raise ValueError('model schema {0} reference not found '
'under /definitions'.format(schemaobjref))
model = mods.get(modelname)
if model.get('type') != 'object':
raise ValueError('model schema {0} must be type object'.format(modelname))
if 'properties' not in model:
raise ValueError('model schema {0} must have properties fields'.format(modelname))
modelprops = model.get('properties')
if 'errorMessage' not in modelprops:
raise ValueError('model schema {0} must have errorMessage as a property to '
'match AWS convention. If pattern is not set, .+ will '
'be used'.format(modelname))
def _validate_lambda_funcname_format(self):
'''
Checks if the lambda function name format contains only known elements
:return: True on success, ValueError raised on error
'''
try:
if self._lambda_funcname_format:
known_kwargs = dict(stage='',
api='',
resource='',
method='')
self._lambda_funcname_format.format(**known_kwargs)
return True
except Exception:
raise ValueError('Invalid lambda_funcname_format {0}. Please review '
'documentation for known substitutable keys'.format(self._lambda_funcname_format))
def _validate_swagger_file(self):
'''
High level check/validation of the input swagger file based on
https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md
This is not a full schema compliance check, but rather make sure that the input file (YAML or
JSON) can be read into a dictionary, and we check for the content of the Swagger Object for version
and info.
'''
# check for any invalid fields for Swagger Object V2
for field in self._cfg:
if (field not in _Swagger.SWAGGER_OBJ_V2_FIELDS and
not _Swagger.VENDOR_EXT_PATTERN.match(field)):
raise ValueError('Invalid Swagger Object Field: {0}'.format(field))
# check for Required Swagger fields by Saltstack boto apigateway state
for field in _Swagger.SWAGGER_OBJ_V2_FIELDS_REQUIRED:
if field not in self._cfg:
raise ValueError('Missing Swagger Object Field: {0}'.format(field))
# check for Swagger Version
self._swagger_version = self._cfg.get('swagger')
if self._swagger_version not in _Swagger.SWAGGER_VERSIONS_SUPPORTED:
raise ValueError('Unsupported Swagger version: {0},'
'Supported versions are {1}'.format(self._swagger_version,
_Swagger.SWAGGER_VERSIONS_SUPPORTED))
log.info(type(self._models))
self._validate_error_response_model(self.paths, self._models())
@property
def md5_filehash(self):
'''
returns md5 hash for the swagger file
'''
return self._md5_filehash
@property
def info(self):
'''
returns the swagger info object as a dictionary
'''
info = self._cfg.get('info')
if not info:
raise ValueError('Info Object has no values')
return info
@property
def info_json(self):
'''
returns the swagger info object as a pretty printed json string.
'''
return _dict_to_json_pretty(self.info)
@property
def rest_api_name(self):
'''
returns the name of the api
'''
return self._api_name
@property
def rest_api_version(self):
'''
returns the version field in the swagger info object
'''
version = self.info.get('version')
if not version:
raise ValueError('Missing version value in Info Object')
return version
def _models(self):
'''
returns an iterator for the models specified in the swagger file
'''
models = self._cfg.get('definitions')
if not models:
raise ValueError('Definitions Object has no values, You need to define them in your swagger file')
return models
def models(self):
'''
generator to return the tuple of model and its schema to create on aws.
'''
model_dict = self._build_all_dependencies()
while True:
model = self._get_model_without_dependencies(model_dict)
if not model:
break
yield (model, self._models().get(model))
@property
def paths(self):
'''
returns an iterator for the relative resource paths specified in the swagger file
'''
paths = self._cfg.get('paths')
if not paths:
raise ValueError('Paths Object has no values, You need to define them in your swagger file')
for path in paths:
if not path.startswith('/'):
raise ValueError('Path object {0} should start with /. Please fix it'.format(path))
return six.iteritems(paths)
@property
def basePath(self):
'''
returns the base path field as defined in the swagger file
'''
basePath = self._cfg.get('basePath', '')
return basePath
@property
def restApiId(self):
'''
returns the rest api id as returned by AWS on creation of the rest api
'''
return self._restApiId
@restApiId.setter
def restApiId(self, restApiId):
'''
allows the assignment of the rest api id on creation of the rest api
'''
self._restApiId = restApiId
@property
def deployment_label_json(self):
'''
this property returns the unique description in pretty printed json for
a particular api deployment
'''
return _dict_to_json_pretty(self.deployment_label)
@property
def deployment_label(self):
'''
this property returns the deployment label dictionary (mainly used by
stage description)
'''
label = dict()
label['swagger_info_object'] = self.info
label['api_name'] = self.rest_api_name
label['swagger_file'] = os.path.basename(self._swagger_file)
label['swagger_file_md5sum'] = self.md5_filehash
return label
# methods to interact with boto_apigateway execution modules
def _one_or_more_stages_remain(self, deploymentId):
'''
Helper function to find whether there are other stages still associated with a deployment
'''
stages = __salt__['boto_apigateway.describe_api_stages'](restApiId=self.restApiId,
deploymentId=deploymentId,
**self._common_aws_args).get('stages')
return bool(stages)
def no_more_deployments_remain(self):
'''
Helper function to find whether there are deployments left with stages associated
'''
no_more_deployments = True
deployments = __salt__['boto_apigateway.describe_api_deployments'](restApiId=self.restApiId,
**self._common_aws_args).get('deployments')
if deployments:
for deployment in deployments:
deploymentId = deployment.get('id')
stages = __salt__['boto_apigateway.describe_api_stages'](restApiId=self.restApiId,
deploymentId=deploymentId,
**self._common_aws_args).get('stages')
if stages:
no_more_deployments = False
break
return no_more_deployments
def _get_current_deployment_id(self):
'''
Helper method to find the deployment id that the stage name is currently assocaited with.
'''
deploymentId = ''