awspub 0.0.10__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
awspub/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from awspub.api import cleanup, create, list, publish, verify
2
+
3
+ __all__ = ["create", "list", "publish", "cleanup", "verify"]
awspub/api.py ADDED
@@ -0,0 +1,165 @@
1
+ import logging
2
+ import pathlib
3
+ from typing import Dict, Iterator, List, Optional, Tuple
4
+
5
+ from awspub.context import Context
6
+ from awspub.image import Image, _ImageInfo
7
+ from awspub.s3 import S3
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ def _images_grouped(
13
+ images: List[Tuple[str, Image, Dict[str, _ImageInfo]]], group: Optional[str]
14
+ ) -> Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, Dict[str, str]]]]:
15
+ """
16
+ Group the given images by name and by group
17
+
18
+ :param images: the images
19
+ :type images: List[Tuple[str, Image, Dict[str, _ImageInfo]]]
20
+ :param group: a optional group name
21
+ :type group: Optional[str]
22
+ :return: the images grouped by name and by group
23
+ :rtype: Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, Dict[str, str]]]
24
+ """
25
+ images_by_name: Dict[str, Dict[str, str]] = dict()
26
+ images_by_group: Dict[str, Dict[str, Dict[str, str]]] = dict()
27
+ for image_name, image, image_result in images:
28
+ images_region_id: Dict[str, str] = {key: val.image_id for (key, val) in image_result.items()}
29
+ images_by_name[image_name] = images_region_id
30
+ for image_group in image.conf.get("groups", []):
31
+ if group and image_group != group:
32
+ continue
33
+ if not images_by_group.get(image_group):
34
+ images_by_group[image_group] = {}
35
+ images_by_group[image_group][image_name] = images_region_id
36
+ return images_by_name, images_by_group
37
+
38
+
39
+ def _images_filtered(context: Context, group: Optional[str]) -> Iterator[Tuple[str, Image]]:
40
+ """
41
+ Filter the images from ctx based on the given args
42
+
43
+ :param context: the context
44
+ :type context: a awspub.context.Context instance
45
+ :param group: a optional group name
46
+ :type group: Optional[str]
47
+ """
48
+ for image_name in context.conf["images"].keys():
49
+ image = Image(context, image_name)
50
+ if group:
51
+ # limit the images to process to the group given on the command line
52
+ if group not in image.conf.get("groups", []):
53
+ logger.info(f"skipping image {image_name} because not part of group {group}")
54
+ continue
55
+
56
+ logger.info(f"processing image {image_name} (group: {group})")
57
+ yield image_name, image
58
+
59
+
60
+ def create(
61
+ config: pathlib.Path, config_mapping: pathlib.Path, group: Optional[str]
62
+ ) -> Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, Dict[str, str]]]]:
63
+ """
64
+ Create images in the partition of the used account based on
65
+ the given configuration file and the config mapping
66
+
67
+ :param config: the configuration file path
68
+ :type config: pathlib.Path
69
+ :param config_mapping: the config template mapping file path
70
+ :type config_mapping: pathlib.Path
71
+ :param group: only handles images from given group
72
+ :type group: Optional[str]
73
+ :return: the images grouped by name and by group
74
+ :rtype: Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, Dict[str, str]]]
75
+ """
76
+
77
+ ctx = Context(config, config_mapping)
78
+ s3 = S3(ctx)
79
+ s3.upload_file(ctx.conf["source"]["path"])
80
+ images: List[Tuple[str, Image, Dict[str, _ImageInfo]]] = []
81
+ for image_name, image in _images_filtered(ctx, group):
82
+ image_result: Dict[str, _ImageInfo] = image.create()
83
+ images.append((image_name, image, image_result))
84
+ images_by_name, images_by_group = _images_grouped(images, group)
85
+ return images_by_name, images_by_group
86
+
87
+
88
+ def list(
89
+ config: pathlib.Path, config_mapping: pathlib.Path, group: Optional[str]
90
+ ) -> Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, Dict[str, str]]]]:
91
+ """
92
+ List images in the partition of the used account based on
93
+ the given configuration file and the config mapping
94
+
95
+ :param config: the configuration file path
96
+ :type config: pathlib.Path
97
+ :param config_mapping: the config template mapping file path
98
+ :type config_mapping: pathlib.Path
99
+ :param group: only handles images from given group
100
+ :type group: Optional[str]
101
+ :return: the images grouped by name and by group
102
+ :rtype: Tuple[Dict[str, Dict[str, str]], Dict[str, Dict[str, Dict[str, str]]]
103
+ """
104
+ ctx = Context(config, config_mapping)
105
+ images: List[Tuple[str, Image, Dict[str, _ImageInfo]]] = []
106
+ for image_name, image in _images_filtered(ctx, group):
107
+ image_result: Dict[str, _ImageInfo] = image.list()
108
+ images.append((image_name, image, image_result))
109
+
110
+ images_by_name, images_by_group = _images_grouped(images, group)
111
+ return images_by_name, images_by_group
112
+
113
+
114
+ def publish(config: pathlib.Path, config_mapping: pathlib.Path, group: Optional[str]):
115
+ """
116
+ Make available images in the partition of the used account based on
117
+ the given configuration file public
118
+
119
+ :param config: the configuration file path
120
+ :type config: pathlib.Path
121
+ :param config_mapping: the config template mapping file path
122
+ :type config_mapping: pathlib.Path
123
+ :param group: only handles images from given group
124
+ :type group: Optional[str]
125
+ """
126
+ ctx = Context(config, config_mapping)
127
+ for image_name, image in _images_filtered(ctx, group):
128
+ image.publish()
129
+
130
+
131
+ def cleanup(config: pathlib.Path, config_mapping: pathlib.Path, group: Optional[str]):
132
+ """
133
+ Cleanup available images in the partition of the used account based on
134
+ the given configuration file
135
+
136
+ :param config: the configuration file path
137
+ :type config: pathlib.Path
138
+ :param config_mapping: the config template mapping file path
139
+ :type config_mapping: pathlib.Path
140
+ :param group: only handles images from given group
141
+ :type group: Optional[str]
142
+ """
143
+ ctx = Context(config, config_mapping)
144
+ for image_name, image in _images_filtered(ctx, group):
145
+ image.cleanup()
146
+
147
+
148
+ def verify(config: pathlib.Path, config_mapping: pathlib.Path, group: Optional[str]) -> Dict[str, Dict]:
149
+ """
150
+ Verify available images in the partition of the used account based on
151
+ the given configuration file.
152
+ This is EXPERIMENTAL and doesn't work reliable yet!
153
+
154
+ :param config: the configuration file path
155
+ :type config: pathlib.Path
156
+ :param config_mapping: the config template mapping file path
157
+ :type config_mapping: pathlib.Path
158
+ :param group: only handles images from given group
159
+ :type group: Optional[str]
160
+ """
161
+ problems: Dict[str, Dict] = dict()
162
+ ctx = Context(config, config_mapping)
163
+ for image_name, image in _images_filtered(ctx, group):
164
+ problems[image_name] = image.verify()
165
+ return problems
awspub/cli/__init__.py ADDED
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/python3
2
+
3
+ import argparse
4
+ import json
5
+ import logging
6
+ import pathlib
7
+ import sys
8
+
9
+ import awspub
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def _create(args) -> None:
15
+ """
16
+ Create images based on the given configuration and write json
17
+ data to the given output
18
+ """
19
+ images_by_name, images_by_group = awspub.create(args.config, args.config_mapping, args.group)
20
+ images_json = json.dumps({"images": images_by_name, "images-by-group": images_by_group}, indent=4)
21
+ args.output.write(images_json)
22
+
23
+
24
+ def _list(args) -> None:
25
+ """
26
+ List images based on the given configuration and write json
27
+ data to the given output
28
+ """
29
+ images_by_name, images_by_group = awspub.list(args.config, args.config_mapping, args.group)
30
+ images_json = json.dumps({"images": images_by_name, "images-by-group": images_by_group}, indent=4)
31
+ args.output.write(images_json)
32
+
33
+
34
+ def _verify(args) -> None:
35
+ """
36
+ Verify available images against configuration
37
+ """
38
+ problems = awspub.verify(args.config, args.config_mapping, args.group)
39
+ args.output.write((json.dumps({"problems": problems}, indent=4)))
40
+
41
+
42
+ def _cleanup(args) -> None:
43
+ """
44
+ Cleanup available images
45
+ """
46
+ awspub.cleanup(args.config, args.config_mapping, args.group)
47
+
48
+
49
+ def _publish(args) -> None:
50
+ """
51
+ Make available images public
52
+ """
53
+ awspub.publish(args.config, args.config_mapping, args.group)
54
+
55
+
56
+ def _parser():
57
+ parser = argparse.ArgumentParser(description="AWS EC2 publication tool")
58
+ parser.add_argument("--log-level", choices=["info", "debug"], default="info")
59
+ parser.add_argument("--log-file", type=pathlib.Path, help="write log to given file instead of stdout")
60
+ parser.add_argument("--log-console", action="store_true", help="write log to stdout")
61
+ p_sub = parser.add_subparsers(help="sub-command help")
62
+
63
+ # create
64
+ p_create = p_sub.add_parser("create", help="Create images")
65
+ p_create.add_argument(
66
+ "--output", type=argparse.FileType("w+"), help="output file path. defaults to stdout", default=sys.stdout
67
+ )
68
+ p_create.add_argument("--config-mapping", type=pathlib.Path, help="the image config template mapping file path")
69
+ p_create.add_argument("--group", type=str, help="only handles images from given group")
70
+ p_create.add_argument("config", type=pathlib.Path, help="the image configuration file path")
71
+ p_create.set_defaults(func=_create)
72
+
73
+ # list
74
+ p_list = p_sub.add_parser("list", help="List images (but don't modify anything)")
75
+ p_list.add_argument(
76
+ "--output", type=argparse.FileType("w+"), help="output file path. defaults to stdout", default=sys.stdout
77
+ )
78
+ p_list.add_argument("--config-mapping", type=pathlib.Path, help="the image config template mapping file path")
79
+ p_list.add_argument("--group", type=str, help="only handles images from given group")
80
+ p_list.add_argument("config", type=pathlib.Path, help="the image configuration file path")
81
+ p_list.set_defaults(func=_list)
82
+
83
+ # verify
84
+ p_verify = p_sub.add_parser("verify", help="Verify images")
85
+ p_verify.add_argument(
86
+ "--output", type=argparse.FileType("w+"), help="output file path. defaults to stdout", default=sys.stdout
87
+ )
88
+ p_verify.add_argument("--config-mapping", type=pathlib.Path, help="the image config template mapping file path")
89
+ p_verify.add_argument("--group", type=str, help="only handles images from given group")
90
+ p_verify.add_argument("config", type=pathlib.Path, help="the image configuration file path")
91
+
92
+ p_verify.set_defaults(func=_verify)
93
+
94
+ # cleanup
95
+ p_cleanup = p_sub.add_parser("cleanup", help="Cleanup images")
96
+ p_cleanup.add_argument(
97
+ "--output", type=argparse.FileType("w+"), help="output file path. defaults to stdout", default=sys.stdout
98
+ )
99
+ p_cleanup.add_argument("--config-mapping", type=pathlib.Path, help="the image config template mapping file path")
100
+ p_cleanup.add_argument("--group", type=str, help="only handles images from given group")
101
+ p_cleanup.add_argument("config", type=pathlib.Path, help="the image configuration file path")
102
+
103
+ p_cleanup.set_defaults(func=_cleanup)
104
+
105
+ # publish
106
+ p_publish = p_sub.add_parser("publish", help="Publish images")
107
+ p_publish.add_argument(
108
+ "--output", type=argparse.FileType("w+"), help="output file path. defaults to stdout", default=sys.stdout
109
+ )
110
+ p_publish.add_argument("--config-mapping", type=pathlib.Path, help="the image config template mapping file path")
111
+ p_publish.add_argument("--group", type=str, help="only handles images from given group")
112
+ p_publish.add_argument("config", type=pathlib.Path, help="the image configuration file path")
113
+
114
+ p_publish.set_defaults(func=_publish)
115
+
116
+ return parser
117
+
118
+
119
+ def main():
120
+ parser = _parser()
121
+ args = parser.parse_args()
122
+ log_formatter = logging.Formatter("%(asctime)s:%(name)s:%(levelname)s:%(message)s")
123
+ # log level
124
+ loglevel = logging.INFO
125
+ if args.log_level == "debug":
126
+ loglevel = logging.DEBUG
127
+ root_logger = logging.getLogger()
128
+ root_logger.setLevel(loglevel)
129
+ # log file
130
+ if args.log_file:
131
+ file_handler = logging.FileHandler(filename=args.log_file)
132
+ file_handler.setFormatter(log_formatter)
133
+ root_logger.addHandler(file_handler)
134
+ # log console
135
+ if args.log_console:
136
+ console_handler = logging.StreamHandler()
137
+ console_handler.setFormatter(log_formatter)
138
+ root_logger.addHandler(console_handler)
139
+ if "func" not in args:
140
+ sys.exit(parser.print_help())
141
+ args.func(args)
142
+ sys.exit(0)
143
+
144
+
145
+ if __name__ == "__main__":
146
+ main()
awspub/common.py ADDED
@@ -0,0 +1,64 @@
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__)
8
+
9
+
10
+ def _split_partition(val: str) -> Tuple[str, str]:
11
+ """
12
+ Split a string into partition and resource, separated by a colon. If no partition is given, assume "aws"
13
+ :param val: the string to split
14
+ :type val: str
15
+ :return: the partition and the resource
16
+ :rtype: Tuple[str, str]
17
+ """
18
+ if ":" in val:
19
+ partition, resource = val.split(":")
20
+ else:
21
+ # if no partition is given, assume default commercial partition "aws"
22
+ partition = "aws"
23
+ resource = val
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 ADDED
@@ -0,0 +1,216 @@
1
+ import pathlib
2
+ from enum import Enum
3
+ from typing import Dict, List, Literal, Optional
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field, field_validator
6
+
7
+ from awspub.common import _split_partition
8
+
9
+
10
+ class ConfigS3Model(BaseModel):
11
+ """
12
+ S3 configuration.
13
+ This is required for uploading source files (usually .vmdk) to a bucket so
14
+ snapshots can be created out of the s3 file
15
+ """
16
+
17
+ model_config = ConfigDict(extra="forbid")
18
+
19
+ bucket_name: str = Field(description="The S3 bucket name")
20
+
21
+
22
+ class ConfigSourceModel(BaseModel):
23
+ """
24
+ Source configuration.
25
+ This defines the source (usually .vmdk) that is uploaded
26
+ to S3 and then used to create EC2 snapshots in different regions.
27
+ """
28
+
29
+ model_config = ConfigDict(extra="forbid")
30
+
31
+ path: pathlib.Path = Field(description="Path to a local .vmdk image")
32
+ architecture: Literal["x86_64", "arm64"] = Field(description="The architecture of the given .vmdk image")
33
+
34
+
35
+ class ConfigImageMarketplaceSecurityGroupModel(BaseModel):
36
+ """
37
+ Image/AMI Marketplace specific configuration for a security group
38
+ """
39
+
40
+ model_config = ConfigDict(extra="forbid")
41
+
42
+ from_port: int = Field(description="The source port")
43
+ ip_protocol: Literal["tcp", "udp"] = Field(description="The IP protocol (either 'tcp' or 'udp')")
44
+ ip_ranges: List[str] = Field(description="IP ranges to allow, in CIDR format (eg. '192.0.2.0/24')")
45
+ to_port: int = Field(description="The destination port")
46
+
47
+
48
+ class ConfigImageMarketplaceModel(BaseModel):
49
+ """
50
+ Image/AMI Marketplace specific configuration to request new Marketplace versions
51
+ See https://docs.aws.amazon.com/marketplace-catalog/latest/api-reference/ami-products.html
52
+ for further information
53
+ """
54
+
55
+ model_config = ConfigDict(extra="forbid")
56
+
57
+ entity_id: str = Field(description="The entity ID (product ID)")
58
+ # see https://docs.aws.amazon.com/marketplace/latest/userguide/ami-single-ami-products.html#single-ami-marketplace-ami-access # noqa:E501
59
+ access_role_arn: str = Field(
60
+ description="IAM role Amazon Resource Name (ARN) used by AWS Marketplace to access the provided AMI"
61
+ )
62
+ version_title: str = Field(description="The version title. Must be unique across the product")
63
+ release_notes: str = Field(description="The release notes")
64
+ user_name: str = Field(description="The login username to access the operating system")
65
+ scanning_port: int = Field(description="Port to access the operating system (default: 22)", default=22)
66
+ os_name: str = Field(description="Operating system name displayed to Marketplace buyers")
67
+ os_version: str = Field(description="Operating system version displayed to Marketplace buyers")
68
+ usage_instructions: str = Field(
69
+ description=" Instructions for using the AMI, or a link to more information about the AMI"
70
+ )
71
+ recommended_instance_type: str = Field(
72
+ description="The instance type that is recommended to run the service with the AMI and is the "
73
+ "default for 1-click installs of your service"
74
+ )
75
+ security_groups: Optional[List[ConfigImageMarketplaceSecurityGroupModel]]
76
+
77
+
78
+ class ConfigImageSSMParameterModel(BaseModel):
79
+ """
80
+ Image/AMI SSM specific configuration to push parameters of type `aws:ec2:image` to the parameter store
81
+ """
82
+
83
+ model_config = ConfigDict(extra="forbid")
84
+
85
+ name: str = Field(
86
+ description="The fully qualified name of the parameter that you want to add to the system. "
87
+ "A parameter name must be unique within an Amazon Web Services Region"
88
+ )
89
+ description: Optional[str] = Field(
90
+ description="Information about the parameter that you want to add to the system", default=None
91
+ )
92
+ allow_overwrite: Optional[bool] = Field(
93
+ description="allow to overwrite an existing parameter. Useful for keep a 'latest' parameter (default: false)",
94
+ default=False,
95
+ )
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
+
132
+ class ConfigImageModel(BaseModel):
133
+ """
134
+ Image/AMI configuration.
135
+ """
136
+
137
+ model_config = ConfigDict(extra="forbid")
138
+
139
+ description: Optional[str] = Field(description="Optional image description", default=None)
140
+ regions: Optional[List[str]] = Field(
141
+ description="Optional list of regions for this image. If not given, all available regions will"
142
+ "be used from the currently used partition. If a region doesn't exist in the currently used partition,"
143
+ " it will be ignored.",
144
+ default=None,
145
+ )
146
+ separate_snapshot: bool = Field(description="Use a separate snapshot for this image?", default=False)
147
+ billing_products: Optional[List[str]] = Field(description="Optional list of billing codes", default=None)
148
+ boot_mode: Literal["legacy-bios", "uefi", "uefi-preferred"] = Field(
149
+ description="The boot mode. For arm64, this needs to be 'uefi'"
150
+ )
151
+ root_device_name: Optional[str] = Field(description="The root device name", default="/dev/sda1")
152
+ root_device_volume_type: Optional[Literal["gp2", "gp3"]] = Field(
153
+ description="The root device volume type", default="gp3"
154
+ )
155
+ root_device_volume_size: Optional[int] = Field(description="The root device volume size (in GB)", default=8)
156
+ uefi_data: Optional[pathlib.Path] = Field(
157
+ description="Optional path to a non-volatile UEFI variable store (must be already base64 encoded)", default=None
158
+ )
159
+ tpm_support: Optional[Literal["v2.0"]] = Field(
160
+ description="Optional TPM support. If this is set, 'boot_mode' must be 'uefi'", default=None
161
+ )
162
+ imds_support: Optional[Literal["v2.0"]] = Field(description="Optional IMDS support", default=None)
163
+ share: Optional[List[str]] = Field(
164
+ description="Optional list of account IDs the image and snapshot will be shared with. The account"
165
+ "ID can be prefixed with the partition and separated by ':'. Eg 'aws-cn:123456789123'",
166
+ default=None,
167
+ )
168
+ temporary: Optional[bool] = Field(
169
+ description="Optional boolean field indicates that a image is only temporary", default=False
170
+ )
171
+ public: Optional[bool] = Field(
172
+ description="Optional boolean field indicates if the image should be public", default=False
173
+ )
174
+ marketplace: Optional[ConfigImageMarketplaceModel] = Field(
175
+ description="Optional structure containing Marketplace related configuration for the commercial "
176
+ "'aws' partition",
177
+ default=None,
178
+ )
179
+ ssm_parameter: Optional[List[ConfigImageSSMParameterModel]] = Field(
180
+ description="Optional list of SSM parameter paths of type `aws:ec2:image` which will "
181
+ "be pushed to the parameter store",
182
+ default=None,
183
+ )
184
+ groups: Optional[List[str]] = Field(description="Optional list of groups this image is part of", default=[])
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
+ )
189
+
190
+ @field_validator("share")
191
+ @classmethod
192
+ def check_share(cls, v: Optional[List[str]]) -> Optional[List[str]]:
193
+ """
194
+ Make sure the account IDs are valid and if given the partition is correct
195
+ """
196
+ if v is not None:
197
+ for val in v:
198
+ partition, account_id = _split_partition(val)
199
+ if len(account_id) != 12:
200
+ raise ValueError("Account ID must be 12 characters long")
201
+ if partition not in ["aws", "aws-cn", "aws-us-gov"]:
202
+ raise ValueError("Partition must be one of 'aws', 'aws-cn', 'aws-us-gov'")
203
+ return v
204
+
205
+
206
+ class ConfigModel(BaseModel):
207
+ """
208
+ The base model for the whole configuration
209
+ """
210
+
211
+ model_config = ConfigDict(extra="forbid")
212
+
213
+ s3: ConfigS3Model
214
+ source: ConfigSourceModel
215
+ images: Dict[str, ConfigImageModel]
216
+ tags: Optional[Dict[str, str]] = Field(description="Optional Tags to apply to all resources", default={})