Skip to content

Commit a1d7e54

Browse files
authored
feat: add Tags support to Http Api (#1459)
1 parent 77fa3b9 commit a1d7e54

37 files changed

+327
-107
lines changed

samtranslator/model/api/http_api_generator.py

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
AuthProperties = namedtuple("_AuthProperties", ["Authorizers", "DefaultAuthorizer"])
1212
AuthProperties.__new__.__defaults__ = (None, None)
1313
DefaultStageName = "$default"
14+
HttpApiTagName = "httpapi:createdBy"
1415

1516

1617
class HttpApiGenerator(object):
@@ -70,6 +71,7 @@ def _construct_http_api(self):
7071
)
7172

7273
self._add_auth()
74+
self._add_tags()
7375

7476
if self.definition_uri:
7577
http_api.BodyS3Location = self._construct_body_s3_dict()
@@ -83,9 +85,6 @@ def _construct_http_api(self):
8385
"add a 'HttpApi' event to an 'AWS::Serverless::Function'",
8486
)
8587

86-
if self.tags is not None:
87-
http_api.Tags = get_tag_list(self.tags)
88-
8988
return http_api
9089

9190
def _add_auth(self):
@@ -97,7 +96,7 @@ def _add_auth(self):
9796

9897
if self.auth and not self.definition_body:
9998
raise InvalidResourceException(
100-
self.logical_id, "Auth works only with inline Swagger specified in " "'DefinitionBody' property"
99+
self.logical_id, "Auth works only with inline OpenApi specified in the 'DefinitionBody' property."
101100
)
102101

103102
# Make sure keys in the dict are recognized
@@ -107,7 +106,7 @@ def _add_auth(self):
107106
if not OpenApiEditor.is_valid(self.definition_body):
108107
raise InvalidResourceException(
109108
self.logical_id,
110-
"Unable to add Auth configuration because " "'DefinitionBody' does not contain a valid Swagger",
109+
"Unable to add Auth configuration because 'DefinitionBody' does not contain a valid OpenApi definition.",
111110
)
112111
open_api_editor = OpenApiEditor(self.definition_body)
113112
auth_properties = AuthProperties(**self.auth)
@@ -120,6 +119,36 @@ def _add_auth(self):
120119
)
121120
self.definition_body = open_api_editor.openapi
122121

122+
def _add_tags(self):
123+
"""
124+
Adds tags to the Http Api, including a default SAM tag.
125+
"""
126+
if self.tags and not self.definition_body:
127+
raise InvalidResourceException(
128+
self.logical_id, "Tags works only with inline OpenApi specified in the 'DefinitionBody' property."
129+
)
130+
131+
if not self.definition_body:
132+
return
133+
134+
if self.tags and not OpenApiEditor.is_valid(self.definition_body):
135+
raise InvalidResourceException(
136+
self.logical_id,
137+
"Unable to add `Tags` because 'DefinitionBody' does not contain a valid OpenApi definition.",
138+
)
139+
elif not OpenApiEditor.is_valid(self.definition_body):
140+
return
141+
142+
if not self.tags:
143+
self.tags = {}
144+
self.tags[HttpApiTagName] = "SAM"
145+
146+
open_api_editor = OpenApiEditor(self.definition_body)
147+
148+
# authorizers is guaranteed to return a value or raise an exception
149+
open_api_editor.add_tags(self.tags)
150+
self.definition_body = open_api_editor.openapi
151+
123152
def _set_default_authorizer(self, open_api_editor, authorizers, default_authorizer, api_authorizers):
124153
"""
125154
Sets the default authorizer if one is given in the template
@@ -134,7 +163,9 @@ def _set_default_authorizer(self, open_api_editor, authorizers, default_authoriz
134163
if not authorizers.get(default_authorizer):
135164
raise InvalidResourceException(
136165
self.logical_id,
137-
"Unable to set DefaultAuthorizer because '" + default_authorizer + "' was not defined in 'Authorizers'",
166+
"Unable to set DefaultAuthorizer because '"
167+
+ default_authorizer
168+
+ "' was not defined in 'Authorizers'.",
138169
)
139170

140171
for path in open_api_editor.iter_on_path():
@@ -151,7 +182,7 @@ def _get_authorizers(self, authorizers_config, default_authorizer=None):
151182
authorizers = {}
152183

153184
if not isinstance(authorizers_config, dict):
154-
raise InvalidResourceException(self.logical_id, "Authorizers must be a dictionary")
185+
raise InvalidResourceException(self.logical_id, "Authorizers must be a dictionary.")
155186

156187
for authorizer_name, authorizer in authorizers_config.items():
157188
if not isinstance(authorizer, dict):
@@ -179,7 +210,7 @@ def _construct_body_s3_dict(self):
179210
if not self.definition_uri.get("Bucket", None) or not self.definition_uri.get("Key", None):
180211
# DefinitionUri is a dictionary but does not contain Bucket or Key property
181212
raise InvalidResourceException(
182-
self.logical_id, "'DefinitionUri' requires Bucket and Key properties to be specified"
213+
self.logical_id, "'DefinitionUri' requires Bucket and Key properties to be specified."
183214
)
184215
s3_pointer = self.definition_uri
185216

@@ -226,9 +257,6 @@ def _construct_stage(self):
226257
stage.AccessLogSettings = self.access_log_settings
227258
stage.AutoDeploy = True
228259

229-
if self.tags is not None:
230-
stage.Tags = get_tag_list(self.tags)
231-
232260
return stage
233261

234262
def to_cloudformation(self):

samtranslator/open_api/open_api.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class OpenApiEditor(object):
1616
"""
1717

1818
_X_APIGW_INTEGRATION = "x-amazon-apigateway-integration"
19+
_X_APIGW_TAG_VALUE = "x-amazon-apigateway-tag-value"
1920
_CONDITIONAL_IF = "Fn::If"
2021
_X_ANY_METHOD = "x-amazon-apigateway-any-method"
2122
_ALL_HTTP_METHODS = ["OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", "PATCH"]
@@ -26,8 +27,8 @@ def __init__(self, doc):
2627
Initialize the class with a swagger dictionary. This class creates a copy of the Swagger and performs all
2728
modifications on this copy.
2829
29-
:param dict doc: Swagger document as a dictionary
30-
:raises ValueError: If the input Swagger document does not meet the basic Swagger requirements.
30+
:param dict doc: OpenApi document as a dictionary
31+
:raises ValueError: If the input OpenApi document does not meet the basic OpenApi requirements.
3132
"""
3233
if not OpenApiEditor.is_valid(doc):
3334
raise ValueError(
@@ -39,6 +40,7 @@ def __init__(self, doc):
3940
self.paths = self._doc["paths"]
4041
self.security_schemes = self._doc.get("components", {}).get("securitySchemes", {})
4142
self.definitions = self._doc.get("definitions", {})
43+
self.tags = self._doc.get("tags", [])
4244

4345
def get_path(self, path):
4446
"""
@@ -358,6 +360,22 @@ def _set_method_authorizer(self, path, method_name, authorizer_name, authorizers
358360
if security:
359361
method_definition["security"] = security
360362

363+
def add_tags(self, tags):
364+
"""
365+
Adds tags to the OpenApi definition using an ApiGateway extension for tag values.
366+
367+
:param dict tags: dictionary of tagName:tagValue pairs.
368+
"""
369+
for name, value in tags.items():
370+
# find an existing tag with this name if it exists
371+
existing_tag = next((existing_tag for existing_tag in self.tags if existing_tag.get("name") == name), None)
372+
if existing_tag:
373+
# overwrite tag value for an existing tag
374+
existing_tag[self._X_APIGW_TAG_VALUE] = value
375+
else:
376+
tag = {"name": name, self._X_APIGW_TAG_VALUE: value}
377+
self.tags.append(tag)
378+
361379
@property
362380
def openapi(self):
363381
"""
@@ -369,6 +387,9 @@ def openapi(self):
369387
# Make sure any changes to the paths are reflected back in output
370388
self._doc["paths"] = self.paths
371389

390+
if self.tags:
391+
self._doc["tags"] = self.tags
392+
372393
if self.security_schemes:
373394
self._doc.setdefault("components", {})
374395
self._doc["components"]["securitySchemes"] = self.security_schemes
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Resources:
2+
Api:
3+
Type: AWS::Serverless::HttpApi
4+
Properties:
5+
DefinitionBody:
6+
invalid: def_body
7+
Tags:
8+
Tag: value
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Resources:
2+
Api:
3+
Type: AWS::Serverless::HttpApi
4+
Properties:
5+
DefinitionUri: s3://bucket/key
6+
Tags:
7+
Tag: value

tests/translator/input/http_api_def_uri.yaml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ Resources:
33
Type: AWS::Serverless::HttpApi
44
Properties:
55
DefinitionUri: s3://bucket/key
6-
Tags:
7-
Tag: value
86
StageName: !Join ["", ["Stage", "Name"]]
97

108
MyApi2:
@@ -14,8 +12,6 @@ Resources:
1412
Bucket: bucket
1513
Key: key
1614
Version: version
17-
Tags:
18-
Tag: value
1915

2016
Function:
2117
Type: AWS::Serverless::Function

tests/translator/input/http_api_existing_openapi_conditions.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ Resources:
3535
MyApi:
3636
Type: AWS::Serverless::HttpApi
3737
Properties:
38+
Tags:
39+
Tag1: value1
40+
Tag2: value2
3841
Auth:
3942
Authorizers:
4043
OAuth2:
@@ -106,6 +109,9 @@ Resources:
106109
- scope4
107110
responses: {}
108111
openapi: 3.0.1
112+
tags:
113+
- name: Tag1
114+
description: this tag exists, but doesn't have an amazon extension value
109115
components:
110116
securitySchemes:
111117
oauth2Auth:

tests/translator/output/aws-cn/explicit_http_api.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,12 @@
7474
"Ref": "AWS::StackName"
7575
}
7676
},
77+
"tags": [
78+
{
79+
"name": "httpapi:createdBy",
80+
"x-amazon-apigateway-tag-value": "SAM"
81+
}
82+
],
7783
"paths": {
7884
"$default": {
7985
"x-amazon-apigateway-any-method": {
@@ -177,6 +183,12 @@
177183
"Ref": "AWS::StackName"
178184
}
179185
},
186+
"tags": [
187+
{
188+
"name": "httpapi:createdBy",
189+
"x-amazon-apigateway-tag-value": "SAM"
190+
}
191+
],
180192
"paths": {
181193
"$default": {
182194
"x-amazon-apigateway-any-method": {

tests/translator/output/aws-cn/explicit_http_api_minimum.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@
5454
"Ref": "AWS::StackName"
5555
}
5656
},
57+
"tags": [
58+
{
59+
"name": "httpapi:createdBy",
60+
"x-amazon-apigateway-tag-value": "SAM"
61+
}
62+
],
5763
"paths": {},
5864
"openapi": "3.0.1"
5965
}
@@ -99,6 +105,12 @@
99105
"Ref": "AWS::StackName"
100106
}
101107
},
108+
"tags": [
109+
{
110+
"name": "httpapi:createdBy",
111+
"x-amazon-apigateway-tag-value": "SAM"
112+
}
113+
],
102114
"paths": {
103115
"$default": {
104116
"x-amazon-apigateway-any-method": {

tests/translator/output/aws-cn/http_api_def_uri.json

Lines changed: 4 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,7 @@
5757
"Name"
5858
]
5959
]
60-
},
61-
"Tags": [
62-
{
63-
"Value": "value",
64-
"Key": "Tag"
65-
}
66-
]
60+
}
6761
}
6862
},
6963
"FunctionRole": {
@@ -103,13 +97,7 @@
10397
"Ref": "MyApi2"
10498
},
10599
"AutoDeploy": true,
106-
"StageName": "$default",
107-
"Tags": [
108-
{
109-
"Value": "value",
110-
"Key": "Tag"
111-
}
112-
]
100+
"StageName": "$default"
113101
}
114102
},
115103
"MyApi2": {
@@ -119,13 +107,7 @@
119107
"Version": "version",
120108
"Bucket": "bucket",
121109
"Key": "key"
122-
},
123-
"Tags": [
124-
{
125-
"Value": "value",
126-
"Key": "Tag"
127-
}
128-
]
110+
}
129111
}
130112
},
131113
"FunctionApi2Permission": {
@@ -155,13 +137,7 @@
155137
"BodyS3Location": {
156138
"Bucket": "bucket",
157139
"Key": "key"
158-
},
159-
"Tags": [
160-
{
161-
"Value": "value",
162-
"Key": "Tag"
163-
}
164-
]
140+
}
165141
}
166142
}
167143
}

tests/translator/output/aws-cn/http_api_existing_openapi.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,12 @@
101101
"Ref": "AWS::StackName"
102102
}
103103
},
104+
"tags": [
105+
{
106+
"name": "httpapi:createdBy",
107+
"x-amazon-apigateway-tag-value": "SAM"
108+
}
109+
],
104110
"paths": {
105111
"/basic": {
106112
"post": {

0 commit comments

Comments
 (0)