awspub 0.0.7__py3-none-any.whl → 0.0.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- awspub/common.py +47 -1
- awspub/configmodels.py +38 -0
- awspub/exceptions.py +8 -0
- awspub/image.py +16 -21
- awspub/sns.py +105 -0
- awspub/tests/fixtures/config1.yaml +48 -0
- awspub/tests/test_api.py +3 -0
- awspub/tests/test_common.py +19 -1
- awspub/tests/test_image.py +24 -6
- awspub/tests/test_sns.py +189 -0
- {awspub-0.0.7.dist-info → awspub-0.0.9.dist-info}/METADATA +4 -21
- {awspub-0.0.7.dist-info → awspub-0.0.9.dist-info}/RECORD +15 -13
- {awspub-0.0.7.dist-info → awspub-0.0.9.dist-info}/WHEEL +1 -1
- {awspub-0.0.7.dist-info → awspub-0.0.9.dist-info}/LICENSE +0 -0
- {awspub-0.0.7.dist-info → awspub-0.0.9.dist-info}/entry_points.txt +0 -0
awspub/common.py
CHANGED
@@ -1,4 +1,10 @@
|
|
1
|
-
|
1
|
+
import logging
|
2
|
+
from typing import List, Tuple
|
3
|
+
|
4
|
+
import boto3
|
5
|
+
from mypy_boto3_ec2.client import EC2Client
|
6
|
+
|
7
|
+
logger = logging.getLogger(__name__)
|
2
8
|
|
3
9
|
|
4
10
|
def _split_partition(val: str) -> Tuple[str, str]:
|
@@ -16,3 +22,43 @@ def _split_partition(val: str) -> Tuple[str, str]:
|
|
16
22
|
partition = "aws"
|
17
23
|
resource = val
|
18
24
|
return partition, resource
|
25
|
+
|
26
|
+
|
27
|
+
def _get_regions(region_to_query: str, regions_allowlist: List[str]) -> List[str]:
|
28
|
+
"""
|
29
|
+
Get a list of region names querying the `region_to_query` for all regions and
|
30
|
+
then filtering by `regions_allowlist`.
|
31
|
+
If no `regions_allowlist` is given, all queried regions are returned for the
|
32
|
+
current partition.
|
33
|
+
If `regions_allowlist` is given, all regions from that list are returned if
|
34
|
+
the listed region exist in the current partition.
|
35
|
+
Eg. `us-east-1` listed in `regions_allowlist` won't be returned if the current
|
36
|
+
partition is `aws-cn`.
|
37
|
+
:param region_to_query: region name of current partition
|
38
|
+
:type region_to_query: str
|
39
|
+
:praram regions_allowlist: list of regions in config file
|
40
|
+
:type regions_allowlist: List[str]
|
41
|
+
:return: list of regions names
|
42
|
+
:rtype: List[str]
|
43
|
+
"""
|
44
|
+
|
45
|
+
# get all available regions
|
46
|
+
ec2client: EC2Client = boto3.client("ec2", region_name=region_to_query)
|
47
|
+
resp = ec2client.describe_regions()
|
48
|
+
ec2_regions_all = [r["RegionName"] for r in resp["Regions"]]
|
49
|
+
|
50
|
+
if regions_allowlist:
|
51
|
+
# filter out regions that are not available in the current partition
|
52
|
+
regions_allowlist_set = set(regions_allowlist)
|
53
|
+
ec2_regions_all_set = set(ec2_regions_all)
|
54
|
+
regions = list(regions_allowlist_set.intersection(ec2_regions_all_set))
|
55
|
+
diff = regions_allowlist_set.difference(ec2_regions_all_set)
|
56
|
+
if diff:
|
57
|
+
logger.warning(
|
58
|
+
f"regions {diff} listed in regions allowlist are not available in the current partition."
|
59
|
+
" Ignoring those."
|
60
|
+
)
|
61
|
+
else:
|
62
|
+
regions = ec2_regions_all
|
63
|
+
|
64
|
+
return regions
|
awspub/configmodels.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
import pathlib
|
2
|
+
from enum import Enum
|
2
3
|
from typing import Dict, List, Literal, Optional
|
3
4
|
|
4
5
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
@@ -94,6 +95,40 @@ class ConfigImageSSMParameterModel(BaseModel):
|
|
94
95
|
)
|
95
96
|
|
96
97
|
|
98
|
+
class SNSNotificationProtocol(str, Enum):
|
99
|
+
DEFAULT = "default"
|
100
|
+
EMAIL = "email"
|
101
|
+
|
102
|
+
|
103
|
+
class ConfigImageSNSNotificationModel(BaseModel):
|
104
|
+
"""
|
105
|
+
Image/AMI SNS Notification specific configuration to notify subscribers about new images availability
|
106
|
+
"""
|
107
|
+
|
108
|
+
model_config = ConfigDict(extra="forbid")
|
109
|
+
|
110
|
+
subject: str = Field(description="The subject of SNS Notification", min_length=1, max_length=99)
|
111
|
+
message: Dict[SNSNotificationProtocol, str] = Field(
|
112
|
+
description="The body of the message to be sent to subscribers.",
|
113
|
+
default={SNSNotificationProtocol.DEFAULT: ""},
|
114
|
+
)
|
115
|
+
regions: Optional[List[str]] = Field(
|
116
|
+
description="Optional list of regions for sending notification. If not given, regions where the image "
|
117
|
+
"registered will be used from the currently used parition. If a region doesn't exist in the currently "
|
118
|
+
"used partition, it will be ignored.",
|
119
|
+
default=None,
|
120
|
+
)
|
121
|
+
|
122
|
+
@field_validator("message")
|
123
|
+
def check_message(cls, value):
|
124
|
+
# Check message protocols have default key
|
125
|
+
# Message should contain at least a top-level JSON key of “default”
|
126
|
+
# with a value that is a string
|
127
|
+
if SNSNotificationProtocol.DEFAULT not in value:
|
128
|
+
raise ValueError(f"{SNSNotificationProtocol.DEFAULT.value} key is required to send SNS notification")
|
129
|
+
return value
|
130
|
+
|
131
|
+
|
97
132
|
class ConfigImageModel(BaseModel):
|
98
133
|
"""
|
99
134
|
Image/AMI configuration.
|
@@ -148,6 +183,9 @@ class ConfigImageModel(BaseModel):
|
|
148
183
|
)
|
149
184
|
groups: Optional[List[str]] = Field(description="Optional list of groups this image is part of", default=[])
|
150
185
|
tags: Optional[Dict[str, str]] = Field(description="Optional Tags to apply to this image only", default={})
|
186
|
+
sns: Optional[List[Dict[str, ConfigImageSNSNotificationModel]]] = Field(
|
187
|
+
description="Optional list of SNS Notification related configuration", default=None
|
188
|
+
)
|
151
189
|
|
152
190
|
@field_validator("share")
|
153
191
|
@classmethod
|
awspub/exceptions.py
CHANGED
@@ -14,3 +14,11 @@ class BucketDoesNotExistException(Exception):
|
|
14
14
|
def __init__(self, bucket_name: str, *args, **kwargs):
|
15
15
|
msg = f"The bucket named '{bucket_name}' does not exist. You will need to create the bucket before proceeding."
|
16
16
|
super().__init__(msg, *args, **kwargs)
|
17
|
+
|
18
|
+
|
19
|
+
class AWSNotificationException(Exception):
|
20
|
+
pass
|
21
|
+
|
22
|
+
|
23
|
+
class AWSAuthorizationException(Exception):
|
24
|
+
pass
|
awspub/image.py
CHANGED
@@ -9,11 +9,12 @@ from mypy_boto3_ec2.client import EC2Client
|
|
9
9
|
from mypy_boto3_ssm import SSMClient
|
10
10
|
|
11
11
|
from awspub import exceptions
|
12
|
-
from awspub.common import _split_partition
|
12
|
+
from awspub.common import _get_regions, _split_partition
|
13
13
|
from awspub.context import Context
|
14
14
|
from awspub.image_marketplace import ImageMarketplace
|
15
15
|
from awspub.s3 import S3
|
16
16
|
from awspub.snapshot import Snapshot
|
17
|
+
from awspub.sns import SNSNotification
|
17
18
|
|
18
19
|
logger = logging.getLogger(__name__)
|
19
20
|
|
@@ -120,28 +121,11 @@ class Image:
|
|
120
121
|
@property
|
121
122
|
def image_regions(self) -> List[str]:
|
122
123
|
"""
|
123
|
-
Get the image regions.
|
124
|
-
or all available regions.
|
125
|
-
If a region is listed that is not available in the currently used partition,
|
126
|
-
that region will be ignored (eg. having us-east-1 configured but running in the aws-cn
|
127
|
-
partition doesn't include us-east-1 here).
|
124
|
+
Get the image regions.
|
128
125
|
"""
|
129
126
|
if not self._image_regions_cached:
|
130
|
-
|
131
|
-
|
132
|
-
resp = ec2client.describe_regions()
|
133
|
-
image_regions_all = [r["RegionName"] for r in resp["Regions"]]
|
134
|
-
|
135
|
-
if self.conf["regions"]:
|
136
|
-
# filter out regions that are not available in the current partition
|
137
|
-
image_regions_configured_set = set(self.conf["regions"])
|
138
|
-
image_regions_all_set = set(image_regions_all)
|
139
|
-
self._image_regions = list(image_regions_configured_set.intersection(image_regions_all_set))
|
140
|
-
diff = image_regions_configured_set.difference(image_regions_all_set)
|
141
|
-
if diff:
|
142
|
-
logger.warning(f"configured regions {diff} not available in the current partition. Ignoring those.")
|
143
|
-
else:
|
144
|
-
self._image_regions = image_regions_all
|
127
|
+
regions_configured = self.conf["regions"] if "regions" in self.conf else []
|
128
|
+
self._image_regions = _get_regions(self._s3.bucket_region, regions_configured)
|
145
129
|
self._image_regions_cached = True
|
146
130
|
return self._image_regions
|
147
131
|
|
@@ -353,6 +337,13 @@ class Image:
|
|
353
337
|
else:
|
354
338
|
logger.error(f"image {self.image_name} not available in region {region}. can not make public")
|
355
339
|
|
340
|
+
def _sns_publish(self) -> None:
|
341
|
+
"""
|
342
|
+
Publish SNS notifiations about newly available images to subscribers
|
343
|
+
"""
|
344
|
+
|
345
|
+
SNSNotification(self._ctx, self.image_name).publish()
|
346
|
+
|
356
347
|
def cleanup(self) -> None:
|
357
348
|
"""
|
358
349
|
Cleanup/delete the temporary images
|
@@ -556,6 +547,10 @@ class Image:
|
|
556
547
|
f"currently using partition {partition}. Ignoring marketplace config."
|
557
548
|
)
|
558
549
|
|
550
|
+
# send ssn notification
|
551
|
+
if self.conf["sns"]:
|
552
|
+
self._sns_publish()
|
553
|
+
|
559
554
|
def _verify(self, region: str) -> List[ImageVerificationErrors]:
|
560
555
|
"""
|
561
556
|
Verify (but don't modify or create anything) the image in a single region
|
awspub/sns.py
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
"""
|
2
|
+
Methods used to handle notifications for AWS using SNS
|
3
|
+
"""
|
4
|
+
|
5
|
+
import json
|
6
|
+
import logging
|
7
|
+
from typing import Any, Dict, List
|
8
|
+
|
9
|
+
import boto3
|
10
|
+
from botocore.exceptions import ClientError
|
11
|
+
from mypy_boto3_sns.client import SNSClient
|
12
|
+
from mypy_boto3_sts.client import STSClient
|
13
|
+
|
14
|
+
from awspub.common import _get_regions
|
15
|
+
from awspub.context import Context
|
16
|
+
from awspub.exceptions import AWSAuthorizationException, AWSNotificationException
|
17
|
+
from awspub.s3 import S3
|
18
|
+
|
19
|
+
logger = logging.getLogger(__name__)
|
20
|
+
|
21
|
+
|
22
|
+
class SNSNotification(object):
|
23
|
+
"""
|
24
|
+
A data object that contains validation logic and
|
25
|
+
structuring rules for SNS notification JSON
|
26
|
+
"""
|
27
|
+
|
28
|
+
def __init__(self, context: Context, image_name: str):
|
29
|
+
"""
|
30
|
+
Construct a message and verify that it is valid
|
31
|
+
"""
|
32
|
+
self._ctx: Context = context
|
33
|
+
self._image_name: str = image_name
|
34
|
+
self._s3: S3 = S3(context)
|
35
|
+
|
36
|
+
@property
|
37
|
+
def conf(self) -> List[Dict[str, Any]]:
|
38
|
+
"""
|
39
|
+
The sns configuration for the current image (based on "image_name") from context
|
40
|
+
"""
|
41
|
+
return self._ctx.conf["images"][self._image_name]["sns"]
|
42
|
+
|
43
|
+
def _sns_regions(self, topic_config: Dict[Any, Any]) -> List[str]:
|
44
|
+
"""
|
45
|
+
Get the sns regions. Either configured in the sns configuration
|
46
|
+
or all available regions.
|
47
|
+
If a region is listed that is not available in the currently used partition,
|
48
|
+
that region will be ignored (eg. having us-east-1 configured but running in the aws-cn
|
49
|
+
partition doesn't include us-east-1 here).
|
50
|
+
"""
|
51
|
+
|
52
|
+
regions_configured = topic_config["regions"] if "regions" in topic_config else []
|
53
|
+
sns_regions = _get_regions(self._s3.bucket_region, regions_configured)
|
54
|
+
|
55
|
+
return sns_regions
|
56
|
+
|
57
|
+
def _get_topic_arn(self, topic_name: str, region_name: str) -> str:
|
58
|
+
"""
|
59
|
+
Calculate topic ARN based on partition, region, account and topic name
|
60
|
+
:param topic_name: Name of topic
|
61
|
+
:type topic_name: str
|
62
|
+
:param region_name: name of region
|
63
|
+
:type region_name: str
|
64
|
+
:return: return topic ARN
|
65
|
+
:rtype: str
|
66
|
+
"""
|
67
|
+
|
68
|
+
stsclient: STSClient = boto3.client("sts", region_name=region_name)
|
69
|
+
resp = stsclient.get_caller_identity()
|
70
|
+
|
71
|
+
account = resp["Account"]
|
72
|
+
# resp["Arn"] has string format "arn:partition:iam::accountnumber:user/iam_role"
|
73
|
+
partition = resp["Arn"].rsplit(":")[1]
|
74
|
+
|
75
|
+
return f"arn:{partition}:sns:{region_name}:{account}:{topic_name}"
|
76
|
+
|
77
|
+
def publish(self) -> None:
|
78
|
+
"""
|
79
|
+
send notification to subscribers
|
80
|
+
"""
|
81
|
+
|
82
|
+
for topic in self.conf:
|
83
|
+
for topic_name, topic_config in topic.items():
|
84
|
+
for region_name in self._sns_regions(topic_config):
|
85
|
+
snsclient: SNSClient = boto3.client("sns", region_name=region_name)
|
86
|
+
try:
|
87
|
+
snsclient.publish(
|
88
|
+
TopicArn=self._get_topic_arn(topic_name, region_name),
|
89
|
+
Subject=topic_config["subject"],
|
90
|
+
Message=json.dumps(topic_config["message"]),
|
91
|
+
MessageStructure="json",
|
92
|
+
)
|
93
|
+
except ClientError as e:
|
94
|
+
exception_code: str = e.response["Error"]["Code"]
|
95
|
+
if exception_code == "AuthorizationError":
|
96
|
+
raise AWSAuthorizationException(
|
97
|
+
"Profile does not have a permission to send the SNS notification."
|
98
|
+
" Please review the policy."
|
99
|
+
)
|
100
|
+
else:
|
101
|
+
raise AWSNotificationException(str(e))
|
102
|
+
logger.info(
|
103
|
+
f"The SNS notification {topic_config['subject']}"
|
104
|
+
f" for the topic {topic_name} in {region_name} has been sent."
|
105
|
+
)
|
@@ -118,6 +118,54 @@ awspub:
|
|
118
118
|
-
|
119
119
|
name: /awspub-test/param2
|
120
120
|
allow_overwrite: true
|
121
|
+
"test-image-10":
|
122
|
+
boot_mode: "uefi"
|
123
|
+
description: |
|
124
|
+
A test image without a separate snapshot but single sns configs
|
125
|
+
regions:
|
126
|
+
- "us-east-1"
|
127
|
+
sns:
|
128
|
+
- "topic1":
|
129
|
+
subject: "topic1-subject"
|
130
|
+
message:
|
131
|
+
default: "default-message"
|
132
|
+
email: "email-message"
|
133
|
+
regions:
|
134
|
+
- "us-east-1"
|
135
|
+
"test-image-11":
|
136
|
+
boot_mode: "uefi"
|
137
|
+
description: |
|
138
|
+
A test image without a separate snapshot but multiple sns configs
|
139
|
+
regions:
|
140
|
+
- "us-east-1"
|
141
|
+
- "eu-central-1"
|
142
|
+
sns:
|
143
|
+
- "topic1":
|
144
|
+
subject: "topic1-subject"
|
145
|
+
message:
|
146
|
+
default: "default-message"
|
147
|
+
email: "email-message"
|
148
|
+
regions:
|
149
|
+
- "us-east-1"
|
150
|
+
- "topic2":
|
151
|
+
subject: "topic2-subject"
|
152
|
+
message:
|
153
|
+
default: "default-message"
|
154
|
+
regions:
|
155
|
+
- "us-gov-1"
|
156
|
+
- "eu-central-1"
|
157
|
+
"test-image-12":
|
158
|
+
boot_mode: "uefi"
|
159
|
+
description: |
|
160
|
+
A test image without a separate snapshot but single sns configs
|
161
|
+
regions:
|
162
|
+
- "us-east-1"
|
163
|
+
sns:
|
164
|
+
- "topic1":
|
165
|
+
subject: "topic1-subject"
|
166
|
+
message:
|
167
|
+
default: "default-message"
|
168
|
+
email: "email-message"
|
121
169
|
|
122
170
|
tags:
|
123
171
|
name: "foobar"
|
awspub/tests/test_api.py
CHANGED
awspub/tests/test_common.py
CHANGED
@@ -1,6 +1,8 @@
|
|
1
|
+
from unittest.mock import patch
|
2
|
+
|
1
3
|
import pytest
|
2
4
|
|
3
|
-
from awspub.common import _split_partition
|
5
|
+
from awspub.common import _get_regions, _split_partition
|
4
6
|
|
5
7
|
|
6
8
|
@pytest.mark.parametrize(
|
@@ -14,3 +16,19 @@ from awspub.common import _split_partition
|
|
14
16
|
)
|
15
17
|
def test_common__split_partition(input, expected_output):
|
16
18
|
assert _split_partition(input) == expected_output
|
19
|
+
|
20
|
+
|
21
|
+
@pytest.mark.parametrize(
|
22
|
+
"regions_in_partition,configured_regions,expected_output",
|
23
|
+
[
|
24
|
+
(["region-1", "region-2"], ["region-1", "region-3"], ["region-1"]),
|
25
|
+
(["region-1", "region-2", "region-3"], ["region-4", "region-5"], []),
|
26
|
+
(["region-1", "region-2"], [], ["region-1", "region-2"]),
|
27
|
+
],
|
28
|
+
)
|
29
|
+
def test_common__get_regions(regions_in_partition, configured_regions, expected_output):
|
30
|
+
with patch("boto3.client") as bclient_mock:
|
31
|
+
instance = bclient_mock.return_value
|
32
|
+
instance.describe_regions.return_value = {"Regions": [{"RegionName": r} for r in regions_in_partition]}
|
33
|
+
|
34
|
+
assert _get_regions("", configured_regions) == expected_output
|
awspub/tests/test_image.py
CHANGED
@@ -127,16 +127,33 @@ def test_image___get_root_device_snapshot_id(root_device_name, block_device_mapp
|
|
127
127
|
|
128
128
|
|
129
129
|
@pytest.mark.parametrize(
|
130
|
-
|
130
|
+
(
|
131
|
+
"imagename",
|
132
|
+
"partition",
|
133
|
+
"called_mod_image",
|
134
|
+
"called_mod_snapshot",
|
135
|
+
"called_start_change_set",
|
136
|
+
"called_put_parameter",
|
137
|
+
"called_sns_publish",
|
138
|
+
),
|
131
139
|
[
|
132
|
-
("test-image-6", "aws", True, True, False, False),
|
133
|
-
("test-image-7", "aws", False, False, False, False),
|
134
|
-
("test-image-8", "aws", True, True, True, True),
|
135
|
-
("test-image-8", "aws-cn", True, True, False, True),
|
140
|
+
("test-image-6", "aws", True, True, False, False, False),
|
141
|
+
("test-image-7", "aws", False, False, False, False, False),
|
142
|
+
("test-image-8", "aws", True, True, True, True, False),
|
143
|
+
("test-image-8", "aws-cn", True, True, False, True, False),
|
144
|
+
("test-image-10", "aws", False, False, False, False, True),
|
145
|
+
("test-image-11", "aws", False, False, False, False, True),
|
146
|
+
("test-image-12", "aws", False, False, False, False, True),
|
136
147
|
],
|
137
148
|
)
|
138
149
|
def test_image_publish(
|
139
|
-
imagename,
|
150
|
+
imagename,
|
151
|
+
partition,
|
152
|
+
called_mod_image,
|
153
|
+
called_mod_snapshot,
|
154
|
+
called_start_change_set,
|
155
|
+
called_put_parameter,
|
156
|
+
called_sns_publish,
|
140
157
|
):
|
141
158
|
"""
|
142
159
|
Test the publish() for a given image
|
@@ -174,6 +191,7 @@ def test_image_publish(
|
|
174
191
|
assert instance.modify_snapshot_attribute.called == called_mod_snapshot
|
175
192
|
assert instance.start_change_set.called == called_start_change_set
|
176
193
|
assert instance.put_parameter.called == called_put_parameter
|
194
|
+
assert instance.publish.called == called_sns_publish
|
177
195
|
|
178
196
|
|
179
197
|
def test_image__get_zero_images():
|
awspub/tests/test_sns.py
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
import pathlib
|
2
|
+
from unittest.mock import patch
|
3
|
+
|
4
|
+
import botocore.exceptions
|
5
|
+
import pytest
|
6
|
+
|
7
|
+
from awspub import context, exceptions, sns
|
8
|
+
|
9
|
+
curdir = pathlib.Path(__file__).parent.resolve()
|
10
|
+
|
11
|
+
|
12
|
+
@pytest.mark.parametrize(
|
13
|
+
"imagename,called_sns_publish, publish_call_count",
|
14
|
+
[
|
15
|
+
("test-image-10", True, 1),
|
16
|
+
("test-image-11", True, 2),
|
17
|
+
("test-image-12", True, 2),
|
18
|
+
],
|
19
|
+
)
|
20
|
+
def test_sns_publish(imagename, called_sns_publish, publish_call_count):
|
21
|
+
"""
|
22
|
+
Test the send_notification logic
|
23
|
+
"""
|
24
|
+
with patch("boto3.client") as bclient_mock:
|
25
|
+
instance = bclient_mock.return_value
|
26
|
+
ctx = context.Context(curdir / "fixtures/config1.yaml", None)
|
27
|
+
instance.describe_regions.return_value = {
|
28
|
+
"Regions": [{"RegionName": "eu-central-1"}, {"RegionName": "us-east-1"}]
|
29
|
+
}
|
30
|
+
instance.list_buckets.return_value = {"Buckets": [{"Name": "bucket1"}]}
|
31
|
+
|
32
|
+
sns.SNSNotification(ctx, imagename).publish()
|
33
|
+
assert instance.publish.called == called_sns_publish
|
34
|
+
assert instance.publish.call_count == publish_call_count
|
35
|
+
|
36
|
+
|
37
|
+
@pytest.mark.parametrize(
|
38
|
+
"imagename",
|
39
|
+
[
|
40
|
+
("test-image-10"),
|
41
|
+
("test-image-11"),
|
42
|
+
("test-image-12"),
|
43
|
+
],
|
44
|
+
)
|
45
|
+
def test_sns_publish_fail_with_invalid_topic(imagename):
|
46
|
+
"""
|
47
|
+
Test the send_notification logic
|
48
|
+
"""
|
49
|
+
with patch("boto3.client") as bclient_mock:
|
50
|
+
instance = bclient_mock.return_value
|
51
|
+
ctx = context.Context(curdir / "fixtures/config1.yaml", None)
|
52
|
+
instance.describe_regions.return_value = {
|
53
|
+
"Regions": [{"RegionName": "eu-central-1"}, {"RegionName": "us-east-1"}]
|
54
|
+
}
|
55
|
+
instance.list_buckets.return_value = {"Buckets": [{"Name": "bucket1"}]}
|
56
|
+
|
57
|
+
# topic1 is invalid topic
|
58
|
+
def side_effect(*args, **kwargs):
|
59
|
+
topic_arn = kwargs.get("TopicArn")
|
60
|
+
if "topic1" in topic_arn and "us-east-1" in topic_arn:
|
61
|
+
error_reponse = {
|
62
|
+
"Error": {
|
63
|
+
"Code": "NotFoundException",
|
64
|
+
"Message": "An error occurred (NotFound) when calling the Publish operation: "
|
65
|
+
"Topic does not exist.",
|
66
|
+
}
|
67
|
+
}
|
68
|
+
raise botocore.exceptions.ClientError(error_reponse, "")
|
69
|
+
|
70
|
+
instance.publish.side_effect = side_effect
|
71
|
+
|
72
|
+
with pytest.raises(exceptions.AWSNotificationException):
|
73
|
+
sns.SNSNotification(ctx, imagename).publish()
|
74
|
+
|
75
|
+
|
76
|
+
@pytest.mark.parametrize(
|
77
|
+
"imagename",
|
78
|
+
[
|
79
|
+
("test-image-10"),
|
80
|
+
("test-image-11"),
|
81
|
+
("test-image-12"),
|
82
|
+
],
|
83
|
+
)
|
84
|
+
def test_sns_publish_fail_with_unauthorized_user(imagename):
|
85
|
+
"""
|
86
|
+
Test the send_notification logic
|
87
|
+
"""
|
88
|
+
with patch("boto3.client") as bclient_mock:
|
89
|
+
instance = bclient_mock.return_value
|
90
|
+
ctx = context.Context(curdir / "fixtures/config1.yaml", None)
|
91
|
+
instance.describe_regions.return_value = {
|
92
|
+
"Regions": [{"RegionName": "eu-central-1"}, {"RegionName": "us-east-1"}]
|
93
|
+
}
|
94
|
+
instance.list_buckets.return_value = {"Buckets": [{"Name": "bucket1"}]}
|
95
|
+
|
96
|
+
error_reponse = {
|
97
|
+
"Error": {
|
98
|
+
"Code": "AuthorizationError",
|
99
|
+
"Message": "User are not authorized perform SNS Notification service",
|
100
|
+
}
|
101
|
+
}
|
102
|
+
instance.publish.side_effect = botocore.exceptions.ClientError(error_reponse, "")
|
103
|
+
|
104
|
+
with pytest.raises(exceptions.AWSAuthorizationException):
|
105
|
+
sns.SNSNotification(ctx, imagename).publish()
|
106
|
+
|
107
|
+
|
108
|
+
@pytest.mark.parametrize(
|
109
|
+
"imagename, partition, regions_in_partition, expected",
|
110
|
+
[
|
111
|
+
(
|
112
|
+
"test-image-10",
|
113
|
+
"aws-cn",
|
114
|
+
["cn-north1", "cn-northwest-1"],
|
115
|
+
[],
|
116
|
+
),
|
117
|
+
(
|
118
|
+
"test-image-11",
|
119
|
+
"aws",
|
120
|
+
["us-east-1", "eu-central-1"],
|
121
|
+
[
|
122
|
+
"arn:aws:sns:us-east-1:1234:topic1",
|
123
|
+
"arn:aws:sns:eu-central-1:1234:topic2",
|
124
|
+
],
|
125
|
+
),
|
126
|
+
(
|
127
|
+
"test-image-12",
|
128
|
+
"aws",
|
129
|
+
["us-east-1", "eu-central-1"],
|
130
|
+
[
|
131
|
+
"arn:aws:sns:us-east-1:1234:topic1",
|
132
|
+
"arn:aws:sns:eu-central-1:1234:topic1",
|
133
|
+
],
|
134
|
+
),
|
135
|
+
],
|
136
|
+
)
|
137
|
+
def test_sns__get_topic_arn(imagename, partition, regions_in_partition, expected):
|
138
|
+
"""
|
139
|
+
Test the send_notification logic
|
140
|
+
"""
|
141
|
+
with patch("boto3.client") as bclient_mock:
|
142
|
+
instance = bclient_mock.return_value
|
143
|
+
ctx = context.Context(curdir / "fixtures/config1.yaml", None)
|
144
|
+
sns_conf = ctx.conf["images"][imagename]["sns"]
|
145
|
+
instance.describe_regions.return_value = {"Regions": [{"RegionName": r} for r in regions_in_partition]}
|
146
|
+
instance.list_buckets.return_value = {"Buckets": [{"Name": "bucket1"}]}
|
147
|
+
|
148
|
+
instance.get_caller_identity.return_value = {"Account": "1234", "Arn": f"arn:{partition}:iam::1234:user/test"}
|
149
|
+
|
150
|
+
topic_arns = []
|
151
|
+
for topic in sns_conf:
|
152
|
+
for topic_name, topic_conf in topic.items():
|
153
|
+
sns_regions = sns.SNSNotification(ctx, imagename)._sns_regions(topic_conf)
|
154
|
+
for region in sns_regions:
|
155
|
+
res_arn = sns.SNSNotification(ctx, imagename)._get_topic_arn(topic_name, region)
|
156
|
+
topic_arns.append(res_arn)
|
157
|
+
|
158
|
+
assert topic_arns == expected
|
159
|
+
|
160
|
+
|
161
|
+
@pytest.mark.parametrize(
|
162
|
+
"imagename,regions_in_partition,regions_expected",
|
163
|
+
[
|
164
|
+
("test-image-10", ["us-east-1", "eu-west-1"], {"topic1": ["us-east-1"]}),
|
165
|
+
(
|
166
|
+
"test-image-11",
|
167
|
+
["us-east-1", "eu-west-1"],
|
168
|
+
{"topic1": ["us-east-1"], "topic2": []},
|
169
|
+
),
|
170
|
+
("test-image-12", ["eu-northwest-1", "ap-southeast-1"], {"topic1": ["eu-northwest-1", "ap-southeast-1"]}),
|
171
|
+
],
|
172
|
+
)
|
173
|
+
def test_sns_regions(imagename, regions_in_partition, regions_expected):
|
174
|
+
"""
|
175
|
+
Test the regions for a given image
|
176
|
+
"""
|
177
|
+
with patch("boto3.client") as bclient_mock:
|
178
|
+
instance = bclient_mock.return_value
|
179
|
+
instance.describe_regions.return_value = {"Regions": [{"RegionName": r} for r in regions_in_partition]}
|
180
|
+
ctx = context.Context(curdir / "fixtures/config1.yaml", None)
|
181
|
+
sns_conf = ctx.conf["images"][imagename]["sns"]
|
182
|
+
instance.list_buckets.return_value = {"Buckets": [{"Name": "bucket1"}]}
|
183
|
+
|
184
|
+
sns_regions = {}
|
185
|
+
for topic in sns_conf:
|
186
|
+
for topic_name, topic_conf in topic.items():
|
187
|
+
sns_regions[topic_name] = sns.SNSNotification(ctx, imagename)._sns_regions(topic_conf)
|
188
|
+
|
189
|
+
assert sns_regions == regions_expected
|
@@ -1,22 +1,22 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: awspub
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.9
|
4
4
|
Summary: Publish images to AWS EC2
|
5
5
|
Home-page: https://github.com/canonical/awspub
|
6
6
|
License: GPL-3.0-or-later
|
7
7
|
Keywords: AWS,EC2,publication
|
8
8
|
Author: Thomas Bechtold
|
9
9
|
Author-email: thomasbechtold@jpberlin.de
|
10
|
-
Requires-Python: >=3.
|
10
|
+
Requires-Python: >=3.10,<4.0
|
11
11
|
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
13
|
-
Classifier: Programming Language :: Python :: 3.9
|
14
13
|
Classifier: Programming Language :: Python :: 3.10
|
15
14
|
Classifier: Programming Language :: Python :: 3.11
|
16
15
|
Classifier: Programming Language :: Python :: 3.12
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
17
17
|
Requires-Dist: autodoc-pydantic (>=2.0.1,<3.0.0)
|
18
18
|
Requires-Dist: boto3
|
19
|
-
Requires-Dist: boto3-stubs[essential,marketplace-catalog,s3,ssm] (>=1.28.83,<2.0.0)
|
19
|
+
Requires-Dist: boto3-stubs[essential,marketplace-catalog,s3,sns,ssm,sts] (>=1.28.83,<2.0.0)
|
20
20
|
Requires-Dist: pydantic (>=2,<3)
|
21
21
|
Requires-Dist: ruamel-yaml (>=0.18.6,<0.19.0)
|
22
22
|
Project-URL: Repository, https://github.com/canonical/awspub
|
@@ -44,20 +44,3 @@ License
|
|
44
44
|
|
45
45
|
The project uses `GPL-3.0` as license.
|
46
46
|
|
47
|
-
Doing a new release
|
48
|
-
===================
|
49
|
-
|
50
|
-
New releases are mostly automated.
|
51
|
-
|
52
|
-
pypi
|
53
|
-
----
|
54
|
-
|
55
|
-
For a new release on pypi, create a new tag (following semantic versioning)
|
56
|
-
with a `v` as prefix (eg. `v0.2.1`).
|
57
|
-
|
58
|
-
snapstore
|
59
|
-
---------
|
60
|
-
|
61
|
-
The latest git commit will be automatically build and published to the `latest/edge`
|
62
|
-
channel. Manually promote from `latest/edge` to `latest/stable`.
|
63
|
-
|
@@ -1,33 +1,35 @@
|
|
1
1
|
awspub/__init__.py,sha256=7hgLrq6k53yaJrjFe7X5Cm45z3SIc1Vxocb5k3G8xPc,124
|
2
2
|
awspub/api.py,sha256=d1gx9LdqdYXRLf8yZ_spIz_93WhB2GNnCG_x3ABrMkI,6497
|
3
3
|
awspub/cli/__init__.py,sha256=-zCBEbnt5zbvSZ8PxQALpPAy0CiQUf-qZnikJ7U4Sf0,5621
|
4
|
-
awspub/common.py,sha256=
|
5
|
-
awspub/configmodels.py,sha256=
|
4
|
+
awspub/common.py,sha256=RFNPfPAw_C0mtdEyBoWWMP6n6CyESdakXhzAvDoyAfQ,2340
|
5
|
+
awspub/configmodels.py,sha256=_6I_-1YzfJiSY81QWK-T3afiQUMx810lQQ3EjoEVuNg,9396
|
6
6
|
awspub/context.py,sha256=LDkp9Sz5AqRxQq70ICgFIJn5g2qrc5qiVawTyS_rXZE,4064
|
7
|
-
awspub/exceptions.py,sha256=
|
8
|
-
awspub/image.py,sha256=
|
7
|
+
awspub/exceptions.py,sha256=2JUEPhZ3sir2NoAuqteFYlh84LsrD7vaeIpkiWtiBpc,556
|
8
|
+
awspub/image.py,sha256=JVJEOnlGR34QUxH-APiWtxTmF6rp2EV846Uimi8o-uU,26878
|
9
9
|
awspub/image_marketplace.py,sha256=oiD7yNU5quG5CQG9Ql5Ut9hLWA1yewg6qVwTbyadGwc,5314
|
10
10
|
awspub/s3.py,sha256=ivR8DuAkYilph73EjFkTgUelkXxU7pZfosnsHHyoZkQ,11274
|
11
11
|
awspub/snapshot.py,sha256=V5e_07SnmCwEPjRmwZh43spWparhH8X4ugG16uQfGuo,10040
|
12
|
+
awspub/sns.py,sha256=q_XfH_-fMC3giWmGLA9ANspZoI2spiqT9tgpAHjzQi0,3970
|
12
13
|
awspub/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
13
14
|
awspub/tests/fixtures/config-invalid-s3-extra.yaml,sha256=TdgqE-quxgueXS9L8ixsRuG6eTVfqalZ41G3JNCWn58,288
|
14
15
|
awspub/tests/fixtures/config-minimal.yaml,sha256=oHupXHYQXxmqgN2qFCAwvxzR7Bch-3yScrmMXeMIICE,176
|
15
16
|
awspub/tests/fixtures/config-valid-nonawspub.yaml,sha256=Md-YINQQRo3kveikUxk8Co9BYIZfDftmPT2LmIqoTL4,330
|
16
17
|
awspub/tests/fixtures/config1.vmdk,sha256=YlJHVAi5-e5kRSthHXBqB4gxqZsSPbadFE2HigSIoKg,65536
|
17
|
-
awspub/tests/fixtures/config1.yaml,sha256=
|
18
|
+
awspub/tests/fixtures/config1.yaml,sha256=knkwwBiImVa8vIxurFbKP6hAB4-84_L2bXcxwdsraig,4609
|
18
19
|
awspub/tests/fixtures/config2-mapping.yaml,sha256=lqJE0ej9DdGsE8O5dqG5PX7bOJrY4nMciXoOzMzV-so,31
|
19
20
|
awspub/tests/fixtures/config2.yaml,sha256=m2v-n1T-XPGDHyrJXArC_rYV-ZPMr9cgzHkLXiSRuDs,1250
|
20
21
|
awspub/tests/fixtures/config3-duplicate-keys.yaml,sha256=Cn0tTQawpEFocDNpWxDz1651uQa7aw88XjNyPcCG4iQ,324
|
21
|
-
awspub/tests/test_api.py,sha256=
|
22
|
+
awspub/tests/test_api.py,sha256=eC8iqpGFgFygSbqlyWLiLjSW7TwZeEtngJ-g7CPQT7I,2603
|
22
23
|
awspub/tests/test_cli.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
23
|
-
awspub/tests/test_common.py,sha256=
|
24
|
+
awspub/tests/test_common.py,sha256=K61eBmBt8NPqyME8co_dt6dr_yvlw3UZ54klcw7m2RA,1217
|
24
25
|
awspub/tests/test_context.py,sha256=wMXQqj4vi2U3q5w1xPV-stB3mp3K6puUyXhsShJG4wA,3115
|
25
|
-
awspub/tests/test_image.py,sha256=
|
26
|
+
awspub/tests/test_image.py,sha256=B1X4iy4B9o45InnsEbU8YLghhq7BZKvkU1hVC7l0j1k,19523
|
26
27
|
awspub/tests/test_image_marketplace.py,sha256=JP7PrFjix1AyQg7eEaQ-wCROVoIOb873koseniOqGQQ,1456
|
27
28
|
awspub/tests/test_s3.py,sha256=UJL8CQDEvhA42MwPGeSvSbQFj8h86c1LrLFDvcMcRws,2857
|
28
29
|
awspub/tests/test_snapshot.py,sha256=8KPTqGVyzrpivWuq3HE7ZhgtLllcr3rA_3hZcxu2xjg,4123
|
29
|
-
awspub
|
30
|
-
awspub-0.0.
|
31
|
-
awspub-0.0.
|
32
|
-
awspub-0.0.
|
33
|
-
awspub-0.0.
|
30
|
+
awspub/tests/test_sns.py,sha256=XdZh0ETwRHSp_77UFkot-07BlS8pKVMEJIs2HS9EdaQ,6622
|
31
|
+
awspub-0.0.9.dist-info/LICENSE,sha256=9GbrzFQ3rWjVKj-IZnX1kGDsIGIdjc25KGRmAp03Jn0,35150
|
32
|
+
awspub-0.0.9.dist-info/METADATA,sha256=3QPgFi4EUPU2fR5M8t3xU69Sb70QDvmTpwRYzmwCQt0,1405
|
33
|
+
awspub-0.0.9.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
34
|
+
awspub-0.0.9.dist-info/entry_points.txt,sha256=hrQzy9P5yO58nj6W0UDPdQPUTqEkQLpMvuyDDRu7LRQ,42
|
35
|
+
awspub-0.0.9.dist-info/RECORD,,
|
File without changes
|
File without changes
|