awspub 0.0.4__py3-none-any.whl → 0.0.6__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 ADDED
@@ -0,0 +1,18 @@
1
+ from typing import Tuple
2
+
3
+
4
+ def _split_partition(val: str) -> Tuple[str, str]:
5
+ """
6
+ Split a string into partition and resource, separated by a colon. If no partition is given, assume "aws"
7
+ :param val: the string to split
8
+ :type val: str
9
+ :return: the partition and the resource
10
+ :rtype: Tuple[str, str]
11
+ """
12
+ if ":" in val:
13
+ partition, resource = val.split(":")
14
+ else:
15
+ # if no partition is given, assume default commercial partition "aws"
16
+ partition = "aws"
17
+ resource = val
18
+ return partition, resource
awspub/configmodels.py CHANGED
@@ -1,7 +1,9 @@
1
1
  import pathlib
2
2
  from typing import Dict, List, Literal, Optional
3
3
 
4
- from pydantic import BaseModel, ConfigDict, Field
4
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
5
+
6
+ from awspub.common import _split_partition
5
7
 
6
8
 
7
9
  class ConfigS3Model(BaseModel):
@@ -101,7 +103,9 @@ class ConfigImageModel(BaseModel):
101
103
 
102
104
  description: Optional[str] = Field(description="Optional image description", default=None)
103
105
  regions: Optional[List[str]] = Field(
104
- description="Optional list of regions for this image. If not given, all available regions will be used",
106
+ description="Optional list of regions for this image. If not given, all available regions will"
107
+ "be used from the currently used partition. If a region doesn't exist in the currently used partition,"
108
+ " it will be ignored.",
105
109
  default=None,
106
110
  )
107
111
  separate_snapshot: bool = Field(description="Use a separate snapshot for this image?", default=False)
@@ -122,7 +126,9 @@ class ConfigImageModel(BaseModel):
122
126
  )
123
127
  imds_support: Optional[Literal["v2.0"]] = Field(description="Optional IMDS support", default=None)
124
128
  share: Optional[List[str]] = Field(
125
- description="Optional list of account IDs the image and snapshot will be shared with", default=None
129
+ description="Optional list of account IDs the image and snapshot will be shared with. The account"
130
+ "ID can be prefixed with the partition and separated by ':'. Eg 'aws-cn:123456789123'",
131
+ default=None,
126
132
  )
127
133
  temporary: Optional[bool] = Field(
128
134
  description="Optional boolean field indicates that a image is only temporary", default=False
@@ -143,6 +149,21 @@ class ConfigImageModel(BaseModel):
143
149
  groups: Optional[List[str]] = Field(description="Optional list of groups this image is part of", default=[])
144
150
  tags: Optional[Dict[str, str]] = Field(description="Optional Tags to apply to this image only", default={})
145
151
 
152
+ @field_validator("share")
153
+ @classmethod
154
+ def check_share(cls, v: Optional[List[str]]) -> Optional[List[str]]:
155
+ """
156
+ Make sure the account IDs are valid and if given the partition is correct
157
+ """
158
+ if v is not None:
159
+ for val in v:
160
+ partition, account_id = _split_partition(val)
161
+ if len(account_id) != 12:
162
+ raise ValueError("Account ID must be 12 characters long")
163
+ if partition not in ["aws", "aws-cn", "aws-us-gov"]:
164
+ raise ValueError("Partition must be one of 'aws', 'aws-cn', 'aws-us-gov'")
165
+ return v
166
+
146
167
 
147
168
  class ConfigModel(BaseModel):
148
169
  """
awspub/image.py CHANGED
@@ -9,6 +9,7 @@ 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
13
  from awspub.context import Context
13
14
  from awspub.image_marketplace import ImageMarketplace
14
15
  from awspub.s3 import S3
@@ -54,6 +55,7 @@ class Image:
54
55
  self._ctx: Context = context
55
56
  self._image_name: str = image_name
56
57
  self._image_regions: List[str] = []
58
+ self._image_regions_cached: bool = False
57
59
 
58
60
  if self._image_name not in self._ctx.conf["images"].keys():
59
61
  raise ValueError(f"image '{self._image_name}' not found in context configuration")
@@ -119,15 +121,28 @@ class Image:
119
121
  def image_regions(self) -> List[str]:
120
122
  """
121
123
  Get the image regions. Either configured in the image configuration
122
- or all available 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).
123
128
  """
124
- if not self._image_regions:
129
+ if not self._image_regions_cached:
130
+ # get all available regions
131
+ ec2client: EC2Client = boto3.client("ec2", region_name=self._s3.bucket_region)
132
+ resp = ec2client.describe_regions()
133
+ image_regions_all = [r["RegionName"] for r in resp["Regions"]]
134
+
125
135
  if self.conf["regions"]:
126
- self._image_regions = 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.")
127
143
  else:
128
- ec2client: EC2Client = boto3.client("ec2", region_name=self._s3.bucket_region)
129
- resp = ec2client.describe_regions()
130
- self._image_regions = [r["RegionName"] for r in resp["Regions"]]
144
+ self._image_regions = image_regions_all
145
+ self._image_regions_cached = True
131
146
  return self._image_regions
132
147
 
133
148
  @property
@@ -145,16 +160,34 @@ class Image:
145
160
  tags.append({"Key": name, "Value": value})
146
161
  return tags
147
162
 
163
+ def _share_list_filtered(self, share_conf: List[str]) -> List[Dict[str, str]]:
164
+ """
165
+ Get a filtered list of share configurations based on the current partition
166
+ :param share_conf: the share configuration
167
+ :type share_conf: List[str]
168
+ :return: a List of share configurations that is usable by modify_image_attribute()
169
+ :rtype: List[Dict[str, str]]
170
+ """
171
+ # the current partition
172
+ partition_current = boto3.client("ec2").meta.partition
173
+
174
+ share_list: List[Dict[str, str]] = []
175
+ for share in share_conf:
176
+ partition, account_id = _split_partition(share)
177
+ if partition == partition_current:
178
+ share_list.append({"UserId": account_id})
179
+ return share_list
180
+
148
181
  def _share(self, share_conf: List[str], images: Dict[str, _ImageInfo]):
149
182
  """
150
183
  Share images with accounts
151
184
 
152
- :param share_conf: the share configuration. eg. self.conf["share_create"]
185
+ :param share_conf: the share configuration containing list
153
186
  :type share_conf: List[str]
154
187
  :param images: a Dict with region names as keys and _ImageInfo objects as values
155
188
  :type images: Dict[str, _ImageInfo]
156
189
  """
157
- share_list: List[Dict[str, str]] = [{"UserId": user_id} for user_id in share_conf]
190
+ share_list = self._share_list_filtered(share_conf)
158
191
 
159
192
  for region, image_info in images.items():
160
193
  ec2client: EC2Client = boto3.client("ec2", region_name=region)
awspub/s3.py CHANGED
@@ -113,7 +113,7 @@ class S3:
113
113
  )
114
114
  return
115
115
  else:
116
- logger.warn(
116
+ logger.warning(
117
117
  f"'{self._ctx.source_sha256}' in bucket '{self.bucket_name}' "
118
118
  f"already exists but sha256sum does not match. Will be overwritten ..."
119
119
  )
@@ -78,6 +78,11 @@ awspub:
78
78
  public: true
79
79
  tags:
80
80
  key1: value1
81
+ share:
82
+ - "123456789123"
83
+ - "221020170000"
84
+ - "aws:290620200000"
85
+ - "aws-cn:334455667788"
81
86
  marketplace:
82
87
  entity_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
83
88
  access_role_arn: "arn:aws:iam::xxxxxxxxxxxx:role/AWSMarketplaceAccess"
@@ -0,0 +1,16 @@
1
+ import pytest
2
+
3
+ from awspub.common import _split_partition
4
+
5
+
6
+ @pytest.mark.parametrize(
7
+ "input,expected_output",
8
+ [
9
+ ("123456789123", ("aws", "123456789123")),
10
+ ("aws:123456789123", ("aws", "123456789123")),
11
+ ("aws-cn:123456789123", ("aws-cn", "123456789123")),
12
+ ("aws-us-gov:123456789123", ("aws-us-gov", "123456789123")),
13
+ ],
14
+ )
15
+ def test_common__split_partition(input, expected_output):
16
+ assert _split_partition(input) == expected_output
@@ -40,27 +40,34 @@ def test_snapshot_names(imagename, snapshotname):
40
40
 
41
41
 
42
42
  @pytest.mark.parametrize(
43
- "imagename,regions",
43
+ "imagename,regions_in_partition,regions_expected",
44
44
  [
45
45
  # test-image-1 has 2 regions defined
46
- ("test-image-1", ["region1", "region2"]),
46
+ ("test-image-1", ["region1", "region2"], ["region1", "region2"]),
47
+ # test-image-1 has 2 regions defined and there are more regions in the partition
48
+ ("test-image-1", ["region1", "region2", "region3"], ["region1", "region2"]),
49
+ ("test-image-1", ["region1", "region2"], ["region1", "region2"]),
47
50
  # test-image-2 has no regions defined, so whatever the ec2 client returns should be valid
48
- ("test-image-2", ["all-region-1", "all-region-2"]),
51
+ ("test-image-2", ["all-region-1", "all-region-2"], ["all-region-1", "all-region-2"]),
52
+ # test-image-1 has 2 regions defined, but those regions are not in the partition
53
+ ("test-image-1", ["region3", "region4"], []),
54
+ # test-image-1 has 2 regions defined, but those regions are not partially in the partition
55
+ ("test-image-1", ["region2", "region4"], ["region2"]),
56
+ # test-image-2 has no regions defined and the ec2 client doesn't return any regions
57
+ ("test-image-2", [], []),
49
58
  ],
50
59
  )
51
60
  @patch("awspub.s3.S3.bucket_region", return_value="region1")
52
- def test_image_regions(s3_region_mock, imagename, regions):
61
+ def test_image_regions(s3_region_mock, imagename, regions_in_partition, regions_expected):
53
62
  """
54
63
  Test the regions for a given image
55
64
  """
56
65
  with patch("boto3.client") as bclient_mock:
57
66
  instance = bclient_mock.return_value
58
- instance.describe_regions.return_value = {
59
- "Regions": [{"RegionName": "all-region-1"}, {"RegionName": "all-region-2"}]
60
- }
67
+ instance.describe_regions.return_value = {"Regions": [{"RegionName": r} for r in regions_in_partition]}
61
68
  ctx = context.Context(curdir / "fixtures/config1.yaml", None)
62
69
  img = image.Image(ctx, imagename)
63
- assert img.image_regions == regions
70
+ assert sorted(img.image_regions) == sorted(regions_expected)
64
71
 
65
72
 
66
73
  @pytest.mark.parametrize(
@@ -77,6 +84,8 @@ def test_image_cleanup(imagename, cleanup):
77
84
  with patch("boto3.client") as bclient_mock:
78
85
  instance = bclient_mock.return_value
79
86
  instance.describe_images.return_value = {"Images": [{"Name": imagename, "Public": False, "ImageId": "ami-123"}]}
87
+ instance.describe_regions.return_value = {"Regions": [{"RegionName": "region1"}, {"RegionName": "region2"}]}
88
+ instance.list_buckets.return_value = {"Buckets": [{"Name": "bucket1"}]}
80
89
  ctx = context.Context(curdir / "fixtures/config1.yaml", None)
81
90
  img = image.Image(ctx, imagename)
82
91
  img.cleanup()
@@ -154,6 +163,10 @@ def test_image_publish(
154
163
  ]
155
164
  }
156
165
  instance.get_parameters.return_value = {"Parameters": []}
166
+ instance.describe_regions.return_value = {
167
+ "Regions": [{"RegionName": "eu-central-1"}, {"RegionName": "us-east-1"}]
168
+ }
169
+ instance.list_buckets.return_value = {"Buckets": [{"Name": "bucket1"}]}
157
170
  ctx = context.Context(curdir / "fixtures/config1.yaml", None)
158
171
  img = image.Image(ctx, imagename)
159
172
  img.publish()
@@ -299,6 +312,10 @@ def test_image_list(available_images, expected):
299
312
  with patch("boto3.client") as bclient_mock:
300
313
  instance = bclient_mock.return_value
301
314
  instance.describe_images.return_value = {"Images": available_images}
315
+ instance.describe_regions.return_value = {
316
+ "Regions": [{"RegionName": "eu-central-1"}, {"RegionName": "us-east-1"}]
317
+ }
318
+ instance.list_buckets.return_value = {"Buckets": [{"Name": "bucket1"}]}
302
319
  ctx = context.Context(curdir / "fixtures/config1.yaml", None)
303
320
  img = image.Image(ctx, "test-image-6")
304
321
  assert img.list() == expected
@@ -330,6 +347,10 @@ def test_image_create_existing(s3_bucket_mock):
330
347
  }
331
348
  ]
332
349
  }
350
+ instance.describe_regions.return_value = {
351
+ "Regions": [{"RegionName": "eu-central-1"}, {"RegionName": "us-east-1"}]
352
+ }
353
+ instance.list_buckets.return_value = {"Buckets": [{"Name": "bucket1"}]}
333
354
  ctx = context.Context(curdir / "fixtures/config1.yaml", None)
334
355
  img = image.Image(ctx, "test-image-6")
335
356
  assert img.create() == {"eu-central-1": image._ImageInfo(image_id="ami-123", snapshot_id="snap-123")}
@@ -375,6 +396,10 @@ def test_image__put_ssm_parameters(
375
396
  instance = bclient_mock.return_value
376
397
  instance.describe_images.return_value = {"Images": describe_images}
377
398
  instance.get_parameters.return_value = {"Parameters": get_parameters}
399
+ instance.describe_regions.return_value = {
400
+ "Regions": [{"RegionName": "eu-central-1"}, {"RegionName": "us-east-1"}]
401
+ }
402
+ instance.list_buckets.return_value = {"Buckets": [{"Name": "bucket1"}]}
378
403
  ctx = context.Context(curdir / "fixtures/config1.yaml", None)
379
404
  img = image.Image(ctx, imagename)
380
405
  img._put_ssm_parameters()
@@ -431,3 +456,23 @@ def test_image__verify(image_found, config, config_image_name, expected_problems
431
456
  img = image.Image(ctx, config_image_name)
432
457
  problems = img._verify("eu-central-1")
433
458
  assert problems == expected_problems
459
+
460
+
461
+ @pytest.mark.parametrize(
462
+ "partition,imagename,share_list_expected",
463
+ [
464
+ ("aws", "test-image-8", [{"UserId": "123456789123"}, {"UserId": "221020170000"}, {"UserId": "290620200000"}]),
465
+ ("aws-cn", "test-image-8", [{"UserId": "334455667788"}]),
466
+ ("aws-us-gov", "test-image-8", []),
467
+ ],
468
+ )
469
+ def test_image__share_list_filtered(partition, imagename, share_list_expected):
470
+ """
471
+ Test _share_list_filtered() for a given image
472
+ """
473
+ with patch("boto3.client") as bclient_mock:
474
+ instance = bclient_mock.return_value
475
+ instance.meta.partition = partition
476
+ ctx = context.Context(curdir / "fixtures/config1.yaml", None)
477
+ img = image.Image(ctx, imagename)
478
+ assert img._share_list_filtered(img.conf["share"]) == share_list_expected
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: awspub
3
- Version: 0.0.4
3
+ Version: 0.0.6
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
@@ -1,31 +1,33 @@
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/configmodels.py,sha256=8JV_fh9VTCSTRcwoqdxXoNFUurn5Q1iTfp-r3u-OLUQ,6869
4
+ awspub/common.py,sha256=M_Ibw8DoAHG3oLoK5qRUggEjI7kJSSslC7r9VySe8vk,562
5
+ awspub/configmodels.py,sha256=DMrC3N8V_zj2SuBRJu27dxxY4Um5rcemWFFqMlA6j9E,7824
5
6
  awspub/context.py,sha256=LDkp9Sz5AqRxQq70ICgFIJn5g2qrc5qiVawTyS_rXZE,4064
6
7
  awspub/exceptions.py,sha256=SbGf9XyiGlj6estlraAwWAKLtuEfzwEuAbHXYiCiJD0,447
7
- awspub/image.py,sha256=FRw8CyYo0RBh-ik4I9W4TmaF0LJ2TpgeddwalidlQKA,25763
8
+ awspub/image.py,sha256=1oJ5x4WljJt4C119qj7b0n9HRXAb3Mi_d_2g5_vRT8o,27451
8
9
  awspub/image_marketplace.py,sha256=oiD7yNU5quG5CQG9Ql5Ut9hLWA1yewg6qVwTbyadGwc,5314
9
- awspub/s3.py,sha256=vnel5UtASHK_mkGuZDnA1IydWCLD_TCZ_WyWwAds9F8,11271
10
+ awspub/s3.py,sha256=ivR8DuAkYilph73EjFkTgUelkXxU7pZfosnsHHyoZkQ,11274
10
11
  awspub/snapshot.py,sha256=V5e_07SnmCwEPjRmwZh43spWparhH8X4ugG16uQfGuo,10040
11
12
  awspub/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
13
  awspub/tests/fixtures/config-invalid-s3-extra.yaml,sha256=TdgqE-quxgueXS9L8ixsRuG6eTVfqalZ41G3JNCWn58,288
13
14
  awspub/tests/fixtures/config-minimal.yaml,sha256=oHupXHYQXxmqgN2qFCAwvxzR7Bch-3yScrmMXeMIICE,176
14
15
  awspub/tests/fixtures/config-valid-nonawspub.yaml,sha256=Md-YINQQRo3kveikUxk8Co9BYIZfDftmPT2LmIqoTL4,330
15
16
  awspub/tests/fixtures/config1.vmdk,sha256=YlJHVAi5-e5kRSthHXBqB4gxqZsSPbadFE2HigSIoKg,65536
16
- awspub/tests/fixtures/config1.yaml,sha256=EdAfnBpZzKBFKcMNv9EYf2qCQ0dk8KHBAXzJUwox370,3132
17
+ awspub/tests/fixtures/config1.yaml,sha256=QUxX7j7SNP40CeSIRFcXm54Ef2CmzgDeWfYYy_THlM0,3256
17
18
  awspub/tests/fixtures/config2-mapping.yaml,sha256=lqJE0ej9DdGsE8O5dqG5PX7bOJrY4nMciXoOzMzV-so,31
18
19
  awspub/tests/fixtures/config2.yaml,sha256=m2v-n1T-XPGDHyrJXArC_rYV-ZPMr9cgzHkLXiSRuDs,1250
19
20
  awspub/tests/fixtures/config3-duplicate-keys.yaml,sha256=Cn0tTQawpEFocDNpWxDz1651uQa7aw88XjNyPcCG4iQ,324
20
21
  awspub/tests/test_api.py,sha256=7MKm2aCtcvHJ0x_o2qinljfL9xFBWnasUnVpBxB37w8,2504
21
22
  awspub/tests/test_cli.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
+ awspub/tests/test_common.py,sha256=kytMUU47uZYYe302XswdO15qX_i1vO2LS5n96--TcSU,478
22
24
  awspub/tests/test_context.py,sha256=wMXQqj4vi2U3q5w1xPV-stB3mp3K6puUyXhsShJG4wA,3115
23
- awspub/tests/test_image.py,sha256=4z7UmcUhFBXCFX-iGz4FbHp7mgm_JNTTJsHSONMbwi0,16400
25
+ awspub/tests/test_image.py,sha256=tMcMx8rnx0q3oeRBA3JSeOHVxnnUGG_AAHVsZ7DWNYw,19083
24
26
  awspub/tests/test_image_marketplace.py,sha256=JP7PrFjix1AyQg7eEaQ-wCROVoIOb873koseniOqGQQ,1456
25
27
  awspub/tests/test_s3.py,sha256=UJL8CQDEvhA42MwPGeSvSbQFj8h86c1LrLFDvcMcRws,2857
26
28
  awspub/tests/test_snapshot.py,sha256=8KPTqGVyzrpivWuq3HE7ZhgtLllcr3rA_3hZcxu2xjg,4123
27
- awspub-0.0.4.dist-info/LICENSE,sha256=9GbrzFQ3rWjVKj-IZnX1kGDsIGIdjc25KGRmAp03Jn0,35150
28
- awspub-0.0.4.dist-info/METADATA,sha256=FnuoxZOfvW7AtbWTySouXFSiQ_FkcMbvd8mtKkczj-s,1773
29
- awspub-0.0.4.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
30
- awspub-0.0.4.dist-info/entry_points.txt,sha256=hrQzy9P5yO58nj6W0UDPdQPUTqEkQLpMvuyDDRu7LRQ,42
31
- awspub-0.0.4.dist-info/RECORD,,
29
+ awspub-0.0.6.dist-info/LICENSE,sha256=9GbrzFQ3rWjVKj-IZnX1kGDsIGIdjc25KGRmAp03Jn0,35150
30
+ awspub-0.0.6.dist-info/METADATA,sha256=0Dk6LfbKHJXh--WbmzrPtLtK75hJNs_G6bpqzN6D2iM,1773
31
+ awspub-0.0.6.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
32
+ awspub-0.0.6.dist-info/entry_points.txt,sha256=hrQzy9P5yO58nj6W0UDPdQPUTqEkQLpMvuyDDRu7LRQ,42
33
+ awspub-0.0.6.dist-info/RECORD,,
File without changes