Skip to content

Commit 8971bf5

Browse files
authored
Merge pull request #67 from toabctl/main-shared
Restrict share by partition
2 parents 78a8caf + 1ce910a commit 8971bf5

File tree

9 files changed

+126
-7
lines changed

9 files changed

+126
-7
lines changed

.github/workflows/pr.yaml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ jobs:
1919
python-version: ${{ matrix.python }}
2020
- name: Install tox and any other packages
2121
run: |
22-
# FIXME: not pining to an exact version results in getting poetry 1.0.10 which doesn't work
23-
pip3 install tox poetry==1.7.1
24-
poetry --version
22+
pip3 install tox
2523
- uses: actions/checkout@v3
2624
- name: Run tox
2725
# Run tox using the version of Python in `PATH`

awspub/common.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import pathlib
22
from typing import Dict, List, Literal, Optional
33

4-
from pydantic import BaseModel, ConfigDict, Field
4+
from pydantic import BaseModel, ConfigDict, Field, field_validator
5+
6+
from awspub.common import _split_partition
57

68

79
class ConfigS3Model(BaseModel):
@@ -124,7 +126,9 @@ class ConfigImageModel(BaseModel):
124126
)
125127
imds_support: Optional[Literal["v2.0"]] = Field(description="Optional IMDS support", default=None)
126128
share: Optional[List[str]] = Field(
127-
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,
128132
)
129133
temporary: Optional[bool] = Field(
130134
description="Optional boolean field indicates that a image is only temporary", default=False
@@ -145,6 +149,21 @@ class ConfigImageModel(BaseModel):
145149
groups: Optional[List[str]] = Field(description="Optional list of groups this image is part of", default=[])
146150
tags: Optional[Dict[str, str]] = Field(description="Optional Tags to apply to this image only", default={})
147151

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+
148167

149168
class ConfigModel(BaseModel):
150169
"""

awspub/image.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from mypy_boto3_ssm import SSMClient
1010

1111
from awspub import exceptions
12+
from awspub.common import _split_partition
1213
from awspub.context import Context
1314
from awspub.image_marketplace import ImageMarketplace
1415
from awspub.s3 import S3
@@ -159,16 +160,34 @@ def _tags(self):
159160
tags.append({"Key": name, "Value": value})
160161
return tags
161162

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+
162181
def _share(self, share_conf: List[str], images: Dict[str, _ImageInfo]):
163182
"""
164183
Share images with accounts
165184
166-
:param share_conf: the share configuration. eg. self.conf["share_create"]
185+
:param share_conf: the share configuration containing list
167186
:type share_conf: List[str]
168187
:param images: a Dict with region names as keys and _ImageInfo objects as values
169188
:type images: Dict[str, _ImageInfo]
170189
"""
171-
share_list: List[Dict[str, str]] = [{"UserId": user_id} for user_id in share_conf]
190+
share_list = self._share_list_filtered(share_conf)
172191

173192
for region, image_info in images.items():
174193
ec2client: EC2Client = boto3.client("ec2", region_name=region)

awspub/tests/fixtures/config1.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,11 @@ awspub:
7878
public: true
7979
tags:
8080
key1: value1
81+
share:
82+
- "123456789123"
83+
- "221020170000"
84+
- "aws:290620200000"
85+
- "aws-cn:334455667788"
8186
marketplace:
8287
entity_id: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
8388
access_role_arn: "arn:aws:iam::xxxxxxxxxxxx:role/AWSMarketplaceAccess"

awspub/tests/test_common.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

awspub/tests/test_image.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,3 +456,23 @@ def test_image__verify(image_found, config, config_image_name, expected_problems
456456
img = image.Image(ctx, config_image_name)
457457
problems = img._verify("eu-central-1")
458458
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
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
awspub:
2+
source:
3+
path: "image.vmdk"
4+
architecture: "x86_64"
5+
s3:
6+
bucket_name: "awspub-toabctl"
7+
images:
8+
"my-custom-image":
9+
boot_mode: "uefi-preferred"
10+
share:
11+
- "123456789123"
12+
- "aws-cn:456789012345"

docs/how_to/publish.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,18 @@ The image needs to be created and then published:
187187
awspub create config.yaml
188188
awspub publish config.yaml
189189
190+
Sharing images
191+
~~~~~~~~~~~~~~
192+
193+
Images can be shared with other AWS accounts. For that, the account IDs of the other accounts are needed.
194+
195+
.. literalinclude:: ../config-samples/config-minimal-share.yaml
196+
:language: yaml
197+
198+
In the above example, the image `my-custom-image` will be shared with the account `1234567890123`
199+
when `awspub` runs in the commercial partition (``aws``, the default). It'll be shared
200+
with the account `456789012345` when `awspub` runs in the the china partition (``aws-cn``).
201+
190202
AWS Marketplace
191203
~~~~~~~~~~~~~~~
192204

0 commit comments

Comments
 (0)