ecs_files_composer 0.9.5__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.
@@ -0,0 +1,8 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright 2020-2022 John Mille<john@compose-x.io>
3
+
4
+ """Top-level package for ECS Files Composer."""
5
+
6
+ __author__ = """John Preston"""
7
+ __email__ = "john@compose-x.io"
8
+ __version__ = "0.9.5"
@@ -0,0 +1,246 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright 2020-2022 John Mille<john@compose-x.io>
3
+
4
+ """AWS module."""
5
+
6
+ import re
7
+
8
+ import boto3
9
+ from boto3.session import Session
10
+ from botocore.exceptions import ClientError
11
+
12
+ from ecs_files_composer import input
13
+ from ecs_files_composer.common import LOG
14
+ from ecs_files_composer.envsubst import expandvars
15
+
16
+
17
+ def create_session_from_creds(tmp_creds: dict, region: str = None):
18
+ """
19
+ Function to easily convert the AssumeRole reply into a boto3 session
20
+ :param tmp_creds:
21
+ :return:
22
+ :rtype boto3.session.Session
23
+ """
24
+ creds = tmp_creds["Credentials"]
25
+ params = {
26
+ "aws_access_key_id": creds["AccessKeyId"],
27
+ "aws_secret_access_key": creds["SecretAccessKey"],
28
+ "aws_session_token": creds["SessionToken"],
29
+ }
30
+ if region:
31
+ params["region_name"] = region
32
+ return boto3.session.Session(**params)
33
+
34
+
35
+ def set_session_from_iam_object(iam_config_object, source_session: Session = None):
36
+ """
37
+ Function to define the client session based on config input
38
+
39
+ :param ecs_files_composer.input.IamOverrideDef iam_config_object:
40
+ :param source_session:
41
+ :return: boto session
42
+ :rtype: boto3.session.Session
43
+ """
44
+ if source_session is None:
45
+ source_session = boto3.Session()
46
+ if not iam_config_object.access_key_id and not iam_config_object.secret_access_key:
47
+ params = {
48
+ "RoleArn": iam_config_object.role_arn,
49
+ "RoleSessionName": f"{iam_config_object.session_name}@AwsResourceHandlerInit",
50
+ }
51
+ if iam_config_object.external_id:
52
+ params["ExternalId"] = iam_config_object.external_id
53
+ tmp_creds = source_session.client("sts").assume_role(**params)
54
+ client_session = create_session_from_creds(
55
+ tmp_creds, region=iam_config_object.region_name
56
+ )
57
+ else:
58
+ client_session = boto3.session.Session(
59
+ aws_access_key_id=iam_config_object.access_key_id,
60
+ aws_secret_access_key=iam_config_object.secret_access_key,
61
+ aws_session_token=iam_config_object.session_token
62
+ if iam_config_object.session_token
63
+ else None,
64
+ )
65
+ return client_session
66
+
67
+
68
+ class AwsResourceHandler:
69
+ """
70
+ Class to handle all AWS related credentials init.
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ role_arn=None,
76
+ external_id=None,
77
+ region=None,
78
+ iam_config_object=None,
79
+ client_session_override=None,
80
+ ):
81
+ """
82
+ :param str role_arn:
83
+ :param str external_id:
84
+ :param str region:
85
+ :param ecs_files_composer.input.IamOverrideDef iam_config_object:
86
+ """
87
+ self.session = Session()
88
+ self.client_session = Session()
89
+ if client_session_override:
90
+ self.client_session = client_session_override
91
+ elif not client_session_override and (role_arn or iam_config_object):
92
+ if role_arn and not iam_config_object:
93
+ params = {
94
+ "RoleArn": role_arn,
95
+ "RoleSessionName": "EcsConfigComposer@AwsResourceHandlerInit",
96
+ }
97
+ if external_id:
98
+ params["ExternalId"] = external_id
99
+ tmp_creds = self.session.client("sts").assume_role(**params)
100
+ self.client_session = create_session_from_creds(
101
+ tmp_creds, region=region
102
+ )
103
+ elif (
104
+ iam_config_object
105
+ and hasattr(iam_config_object, "role_arn")
106
+ and iam_config_object.role_arn
107
+ ):
108
+ self.client_session = set_session_from_iam_object(
109
+ iam_config_object, self.session
110
+ )
111
+
112
+
113
+ class S3Fetcher(AwsResourceHandler):
114
+ """
115
+ Class to handle S3 actions
116
+ """
117
+
118
+ bucket_re = re.compile(r"^s3://(?P<bucket>[a-zA-Z\d\-.]+)/(?P<key>[\S]+)$")
119
+ compose_x_re = re.compile(r"^(?P<bucket>[a-zA-Z\d\-.]+)::(?P<key>[\S]+)$")
120
+
121
+ def __init__(
122
+ self,
123
+ role_arn=None,
124
+ external_id=None,
125
+ region=None,
126
+ iam_config_object=None,
127
+ client_session_override=None,
128
+ ):
129
+ super().__init__(
130
+ role_arn, external_id, region, iam_config_object, client_session_override
131
+ )
132
+
133
+ @property
134
+ def client(self):
135
+ return self.client_session.client("s3")
136
+
137
+ def get_content(
138
+ self,
139
+ s3_uri: str = None,
140
+ s3_bucket: str = None,
141
+ s3_key: str = None,
142
+ composex_uri: str = None,
143
+ ):
144
+ """
145
+ Retrieves a file in a temp dir and returns content
146
+
147
+ :return: The Stream Body for the file, allowing to do various things
148
+ """
149
+
150
+ if s3_uri and self.bucket_re.match(s3_uri):
151
+ s3_bucket = self.bucket_re.match(s3_uri).group("bucket")
152
+ s3_key = self.bucket_re.match(s3_uri).group("key")
153
+ elif composex_uri and self.compose_x_re.match(composex_uri):
154
+ s3_bucket = self.compose_x_re.match(composex_uri).group("bucket")
155
+ s3_key = self.compose_x_re.match(composex_uri).group("key")
156
+ try:
157
+ file_r = self.client.get_object(Bucket=s3_bucket, Key=s3_key)
158
+ file_content = file_r["Body"]
159
+ return file_content
160
+ except self.client.exceptions.NoSuchKey:
161
+ LOG.error(f"Failed to download the file {s3_key} from bucket {s3_bucket}")
162
+ raise
163
+
164
+
165
+ class SsmFetcher(AwsResourceHandler):
166
+ """
167
+ Class to handle SSM actions
168
+ """
169
+
170
+ arn_re = re.compile(
171
+ r"^arn:aws(?:-[a-z]+)?:ssm:[\S]+:[\d]{12}:parameter(?P<name>/[\S]+)$"
172
+ )
173
+
174
+ def __init__(
175
+ self,
176
+ role_arn=None,
177
+ external_id=None,
178
+ region=None,
179
+ iam_config_object=None,
180
+ client_session_override=None,
181
+ ):
182
+ super().__init__(
183
+ role_arn, external_id, region, iam_config_object, client_session_override
184
+ )
185
+ self.client = self.client_session.client("ssm")
186
+
187
+ def get_content(self, parameter_name):
188
+ """
189
+ Import the Content of a given parameter
190
+ If the parameter name is a valid ARN, parses and uses the name from ARN
191
+
192
+ :param parameter_name:
193
+ :return:
194
+ """
195
+ if self.arn_re.match(parameter_name):
196
+ parameter_name = self.arn_re.match(parameter_name).group("name")
197
+ try:
198
+ parameter = self.client.get_parameter(
199
+ Name=parameter_name, WithDecryption=True
200
+ )
201
+ return parameter["Parameter"]["Value"]
202
+ except self.client.exceptions:
203
+ raise
204
+ except ClientError:
205
+ raise
206
+
207
+
208
+ class SecretFetcher(AwsResourceHandler):
209
+ """
210
+ Class to handle Secret Manager actions
211
+ """
212
+
213
+ def __init__(
214
+ self,
215
+ role_arn=None,
216
+ external_id=None,
217
+ region=None,
218
+ iam_config_object=None,
219
+ client_session_override=None,
220
+ ):
221
+ super().__init__(
222
+ role_arn, external_id, region, iam_config_object, client_session_override
223
+ )
224
+ self.client = self.client_session.client("secretsmanager")
225
+
226
+ def get_content(self, secret):
227
+ """
228
+ Import the Content of a given parameter
229
+
230
+ :param input.SecretDef secret:
231
+ :return:
232
+ """
233
+ secret_id = expandvars(secret.secret_id)
234
+ params = {"SecretId": secret_id}
235
+ LOG.debug(f"Retrieving secretsmanager://{secret_id}")
236
+ if secret.version_id:
237
+ params["VersionId"] = secret.version_id
238
+ if secret.version_stage:
239
+ params["VersionStage"] = secret.version_stage
240
+ try:
241
+ parameter = self.client.get_secret_value(**params)
242
+ return parameter["SecretString"]
243
+ except self.client.exceptions:
244
+ raise
245
+ except ClientError:
246
+ raise
@@ -0,0 +1,47 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright 2020-2022 John Mille<john@compose-x.io>
3
+
4
+ """
5
+ Manages certbot certificates download from certbot-aws-store
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from .input import Model
14
+
15
+ from os import makedirs, path
16
+
17
+ from certbot_aws_store.certificate import AcmeCertificate
18
+
19
+ from ecs_files_composer.common import LOG
20
+
21
+
22
+ def handle_certbot_store_certificates(job: Model) -> None:
23
+ """
24
+ Pulls certificates from certbot-aws-store to local filesystem
25
+ If the path does not exist, creates a new directory for download.
26
+ """
27
+ if not job.certbot_store:
28
+ return
29
+ for _hostname, _definition in job.certbot_store.items():
30
+ certificate = AcmeCertificate(
31
+ _hostname,
32
+ None,
33
+ table_name=_definition.table_name if _definition.table_name else None,
34
+ region_name=_definition.table_region_name
35
+ if _definition.table_region_name
36
+ else None,
37
+ )
38
+ if not path.exists(_definition.storage_path):
39
+ makedirs(_definition.storage_path, exist_ok=True)
40
+ try:
41
+ certificate.pull(_definition.storage_path)
42
+ LOG.info("Successfully pulled certificates for %s", _hostname)
43
+ except Exception as error:
44
+ LOG.exception(error)
45
+ LOG.error(
46
+ "Failed to download certificate from certbot-aws-store", _hostname
47
+ )
@@ -0,0 +1,116 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright 2020-2022 John Mille<john@compose-x.io>
3
+
4
+ import pathlib
5
+ import socket
6
+ from os import path
7
+ from typing import Any
8
+
9
+ from OpenSSL import crypto
10
+
11
+ from ecs_files_composer.files_mgmt import File
12
+ from ecs_files_composer.input import X509CertDef
13
+
14
+
15
+ class X509Certificate(X509CertDef):
16
+ """
17
+ Class to wrap actions around a new X509 certificate
18
+ """
19
+
20
+ def __init__(self, **data: Any):
21
+ super().__init__(**data)
22
+ self.key = None
23
+ self.cert = None
24
+ self.key_content = None
25
+ self.cert_content = None
26
+ self.cert_file = None
27
+ self.key_file = None
28
+ self.cert_file_path = None
29
+ self.key_file_path = None
30
+
31
+ def init_cert_paths(self):
32
+ self.cert_file_path = path.abspath(f"{self.dir_path}/{self.cert_file_name}")
33
+ self.key_file_path = path.abspath(f"{self.dir_path}/{self.key_file_name}")
34
+ print(f"Creating {self.dir_path} folder")
35
+ dir_path = pathlib.Path(path.abspath(self.dir_path))
36
+ dir_path.mkdir(parents=True, exist_ok=True)
37
+
38
+ def generate_key(self):
39
+ self.key = crypto.PKey()
40
+ self.key.generate_key(crypto.TYPE_RSA, 4096)
41
+
42
+ def set_common_name(self):
43
+ if self.common_name is None:
44
+ self.common_name = socket.gethostname()
45
+
46
+ def generate_cert(self):
47
+ if not self.common_name:
48
+ self.set_common_name()
49
+ self.cert = crypto.X509()
50
+ self.cert.get_subject().C = self.country_name
51
+ self.cert.get_subject().ST = self.state_or_province_name
52
+ self.cert.get_subject().L = self.locality_name
53
+ self.cert.get_subject().O = self.organization_name
54
+ self.cert.get_subject().OU = self.organization_unit_name
55
+ self.cert.get_subject().CN = self.common_name
56
+ self.cert.get_subject().emailAddress = self.email_address
57
+ self.cert.set_serial_number(0)
58
+ self.cert.gmtime_adj_notBefore(0)
59
+ self.cert.gmtime_adj_notAfter(int(self.validity_end_in_seconds))
60
+ self.cert.set_issuer(self.cert.get_subject())
61
+ self.cert.set_pubkey(self.key)
62
+ self.cert.sign(self.key, "sha512")
63
+
64
+ def generate_cert_content(self):
65
+ if not self.key:
66
+ self.generate_key()
67
+ if not self.cert:
68
+ self.generate_cert()
69
+ self.cert_content = crypto.dump_certificate(
70
+ crypto.FILETYPE_PEM, self.cert
71
+ ).decode("utf-8")
72
+ self.key_content = crypto.dump_privatekey(crypto.FILETYPE_PEM, self.key).decode(
73
+ "utf-8"
74
+ )
75
+
76
+ def set_cert_files(self):
77
+ if not self.cert_content or not self.key_content:
78
+ self.generate_cert_content()
79
+ self.key_file = File().parse_obj(
80
+ {
81
+ "content": self.key_content,
82
+ "path": self.key_file_path,
83
+ "mode": "0600",
84
+ "owner": self.owner,
85
+ "group": self.group,
86
+ }
87
+ )
88
+ self.cert_file = File().parse_obj(
89
+ {
90
+ "content": self.cert_content,
91
+ "path": self.cert_file_path,
92
+ "mode": "0600",
93
+ "owner": self.owner,
94
+ "group": self.group,
95
+ }
96
+ )
97
+
98
+
99
+ def process_x509_certs(job):
100
+ """
101
+
102
+ :param ecs_files_composer.input.Model job:
103
+ :return:
104
+ """
105
+ if not hasattr(job.certificates, "x509") or not job.certificates.x509:
106
+ return
107
+ for cert_path, cert_def in job.certificates.x509.items():
108
+ cert_obj = X509Certificate(
109
+ **(cert_def.dict(by_alias=True)),
110
+ )
111
+ cert_obj.dir_path = cert_path
112
+ cert_obj.init_cert_paths()
113
+ cert_obj.set_cert_files()
114
+ job.certificates.x509[cert_path] = cert_obj
115
+ job.files[cert_obj.cert_file.path] = cert_obj.cert_file
116
+ job.files[cert_obj.key_file_path] = cert_obj.key_file
@@ -0,0 +1,132 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright 2020-2022 John Mille<john@compose-x.io>
3
+
4
+ """Console script for ecs_files_composer."""
5
+ import argparse
6
+ import sys
7
+ from os import environ
8
+
9
+ from ecs_files_composer.common import LOG
10
+ from ecs_files_composer.ecs_files_composer import init_config, start_jobs
11
+
12
+
13
+ def main():
14
+ """Console script for ecs_files_composer."""
15
+ parser = argparse.ArgumentParser()
16
+ options = parser.add_mutually_exclusive_group()
17
+ options.add_argument(
18
+ "-f",
19
+ "--from-file",
20
+ help="Configuration for execution from a file",
21
+ type=str,
22
+ required=False,
23
+ dest="file_path",
24
+ )
25
+ options.add_argument(
26
+ "-e",
27
+ "--from-env-var",
28
+ dest="env_var",
29
+ required=False,
30
+ help="Configuration for execution is in an environment variable",
31
+ )
32
+ options.add_argument(
33
+ "--from-ssm",
34
+ dest="ssm_config",
35
+ help="Configuration for execution is in an SSM Parameter",
36
+ required=False,
37
+ )
38
+ options.add_argument(
39
+ "--from-s3",
40
+ dest="s3_config",
41
+ required=False,
42
+ help="Configuration for execution is in an S3",
43
+ )
44
+ options.add_argument(
45
+ "--from-secrets",
46
+ dest="secret_config",
47
+ required=False,
48
+ help="Configuration for execution is in an AWS Secrets Manager",
49
+ )
50
+ parser.add_argument(
51
+ "--role-arn",
52
+ help="The Role ARN to use for the configuration initialization",
53
+ required=False,
54
+ )
55
+ parser.add_argument(
56
+ "--decode-base64",
57
+ help="Whether the source config is in base64 encoded",
58
+ action="store_true",
59
+ required=False,
60
+ default=False,
61
+ )
62
+ parser.add_argument(
63
+ "--context",
64
+ help="Indicate which context to use (valid: jinja2|plain). Default is jinja2",
65
+ required=False,
66
+ default="jinja2",
67
+ )
68
+ parser.add_argument(
69
+ "--override-init-folder",
70
+ dest="init_folder",
71
+ required=False,
72
+ type=str,
73
+ default="",
74
+ )
75
+ parser.add_argument("_", nargs="*")
76
+ args = parser.parse_args()
77
+ print("Arguments: " + str(args._))
78
+ if not (
79
+ args.env_var or args.ssm_config or args.s3_config or args.file_path
80
+ ) and environ.get("ECS_CONFIG_CONTENT", None):
81
+ LOG.info("Using default env variable ECS_CONFIG_CONTENT")
82
+ config = init_config(
83
+ env_var="ECS_CONFIG_CONTENT",
84
+ decode_base64=bool(environ.get("DECODE_BASE64", False)),
85
+ context=environ.get("context", "jinja2"),
86
+ )
87
+ elif args.env_var:
88
+ config = init_config(
89
+ env_var=args.env_var,
90
+ decode_base64=args.decode_base64,
91
+ context=args.context,
92
+ override_folder=args.init_folder,
93
+ )
94
+ elif args.file_path:
95
+ config = init_config(
96
+ file_path=args.file_path,
97
+ decode_base64=args.decode_base64,
98
+ context=args.context,
99
+ override_folder=args.init_folder,
100
+ )
101
+ elif args.ssm_config:
102
+ config = init_config(
103
+ ssm_parameter=args.ssm_config,
104
+ decode_base64=args.decode_base64,
105
+ context=args.context,
106
+ override_folder=args.init_folder,
107
+ )
108
+ elif args.s3_config:
109
+ config = init_config(
110
+ s3_config=args.s3_config,
111
+ decode_base64=args.decode_base64,
112
+ context=args.context,
113
+ override_folder=args.init_folder,
114
+ )
115
+ elif args.secret_config:
116
+ config = init_config(
117
+ secret_config=args.secret_config,
118
+ decode_base64=args.decode_base64,
119
+ context=args.context,
120
+ override_folder=args.init_folder,
121
+ )
122
+ else:
123
+ raise parser.error(
124
+ "You must specify where the execution configuration comes from or set ECS_CONFIG_CONTENT."
125
+ )
126
+
127
+ start_jobs(config)
128
+ return 0
129
+
130
+
131
+ if __name__ == "__main__":
132
+ sys.exit(main()) # pragma: no cover
@@ -0,0 +1,57 @@
1
+ # SPDX-License-Identifier: MPL-2.0
2
+ # Copyright 2020-2022 John Mille<john@compose-x.io>
3
+
4
+ import logging as logthings
5
+ import sys
6
+ from os import environ
7
+
8
+ from compose_x_common.compose_x_common import keyisset
9
+
10
+
11
+ def setup_logging():
12
+ """Function to setup logging for ECS ComposeX.
13
+ In case this is used in a Lambda function, removes the AWS Lambda default log handler
14
+
15
+ :returns: the_logger
16
+ :rtype: Logger
17
+ """
18
+ level = environ.get("LOGLEVEL")
19
+ default_level = True
20
+ formats = {
21
+ "INFO": logthings.Formatter(
22
+ "%(asctime)s [%(levelname)s], %(message)s",
23
+ "%Y-%m-%d %H:%M:%S",
24
+ ),
25
+ "DEBUG": logthings.Formatter(
26
+ "%(asctime)s [%(levelname)s], %(filename)s.%(lineno)d , %(funcName)s, %(message)s",
27
+ "%Y-%m-%d %H:%M:%S",
28
+ ),
29
+ }
30
+
31
+ if level is not None and isinstance(level, str):
32
+ print("SETTING TO", level.upper())
33
+ logthings.basicConfig(level=level.upper())
34
+ default_level = False
35
+ else:
36
+ logthings.basicConfig(level="INFO")
37
+
38
+ root_logger = logthings.getLogger()
39
+ for h in root_logger.handlers:
40
+ root_logger.removeHandler(h)
41
+ the_logger = logthings.getLogger("EcsComposeX")
42
+
43
+ if not the_logger.handlers:
44
+ if default_level:
45
+ formatter = formats["INFO"]
46
+ elif keyisset(level.upper(), formats):
47
+ formatter = formats[level.upper()]
48
+ else:
49
+ formatter = formats["DEBUG"]
50
+ handler = logthings.StreamHandler(sys.stdout)
51
+ handler.setFormatter(formatter)
52
+ the_logger.addHandler(handler)
53
+
54
+ return the_logger
55
+
56
+
57
+ LOG = setup_logging()