suite-py 1.41.9__py3-none-any.whl → 1.42.0__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.
@@ -1,238 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- import os
3
- import stat
4
- import subprocess
5
- from datetime import datetime
6
- from os import listdir
7
- from os.path import isfile, join
8
-
9
- import yaml
10
- from jinja2 import DebugUndefined, Environment, FileSystemLoader
11
-
12
- from suite_py.lib import logger, metrics
13
- from suite_py.lib.handler import prompt_utils
14
- from suite_py.lib.handler.drone_handler import DroneHandler
15
- from suite_py.lib.handler.git_handler import GitHandler
16
- from suite_py.lib.handler.github_handler import GithubHandler
17
-
18
- NOW = datetime.now().strftime("%Y%m%d%H%M")
19
-
20
-
21
- class Generator:
22
- def __init__(self, project, config, tokens):
23
- self._project = project
24
- self._config = config
25
- self._tokens = tokens
26
- self._umami_path = os.path.join(config.user["projects_home"], "umami")
27
- self._drone_values_path = f"{self._umami_path}/templates/drone/values/"
28
- self._deploy_scripts_values_path = (
29
- f"{self._umami_path}/templates/scripts/values/"
30
- )
31
- self._github = GithubHandler(tokens)
32
-
33
- @metrics.command("generator")
34
- def run(self):
35
-
36
- logger.warning(
37
- "This command requires shellcheck and shfmt, please install these tools before continuing"
38
- )
39
- logger.warning(
40
- "see https://github.com/koalaman/shellcheck#installing and https://github.com/mvdan/sh#shfmt for installation"
41
- )
42
- # move to umami root directory
43
- os.chdir(self._umami_path)
44
-
45
- env = Environment(
46
- loader=FileSystemLoader(self._umami_path),
47
- undefined=DebugUndefined,
48
- trim_blocks=True,
49
- lstrip_blocks=True,
50
- )
51
- env.filters["to_yaml"] = to_yaml
52
- env.add_extension("jinja2.ext.do")
53
-
54
- template_type = prompt_utils.ask_choices(
55
- "What files do you want to generate?", ["drone", "deploy_scripts"]
56
- )
57
- commit_message = f"Autogenerated update {template_type} from suite-py {NOW}"
58
-
59
- autobranch = prompt_utils.ask_confirm(
60
- "Do you want to create branch and PR automatically?", default=False
61
- )
62
-
63
- if autobranch:
64
- autopush = True
65
- else:
66
- autopush = prompt_utils.ask_confirm(
67
- "Do you want to push automatically?", default=False
68
- )
69
-
70
- choice = prompt_utils.ask_questions_input(
71
- "What project do you want to work on? Enter `all` to select them all: ",
72
- self._project,
73
- )
74
-
75
- if choice == "all":
76
- projects = self._get_all_templated_projects(template_type)
77
- else:
78
- projects = choice.split(" ")
79
-
80
- skipped = []
81
- for project in projects:
82
- logger.info(f"{project}: Checking if there are no uncommitted files")
83
- git = GitHandler(project, self._config)
84
- git.check_repo_cloned()
85
- if git.is_dirty():
86
- if not prompt_utils.ask_confirm(
87
- f"{project}: There are changes already present, do you want to continue anyway?",
88
- default=False,
89
- ):
90
- logger.error(f"{project}: skipping the project")
91
- skipped.append(project)
92
- continue
93
-
94
- branch_name = _configure_branch(git, template_type, autobranch, autopush)
95
-
96
- logger.info(f"{project}: Creation in progress")
97
- # launch generate
98
- if template_type == "drone":
99
- self._generate_drone(
100
- project, f"{self._drone_values_path}{project}.yml", env
101
- )
102
- elif template_type == "deploy_scripts":
103
- _generate_build_script(
104
- git.get_path(),
105
- f"{self._deploy_scripts_values_path}{project}.yml",
106
- env,
107
- )
108
- _generate_deploy_script(
109
- git.get_path(),
110
- f"{self._deploy_scripts_values_path}{project}.yml",
111
- env,
112
- )
113
- logger.info(f"{project}: Creation completed")
114
-
115
- if autobranch or autopush:
116
- # controllo che sia cambiato effettivamente qualcosa
117
- if git.is_dirty(untracked=True):
118
- self._git_operations(
119
- git, autobranch, autopush, branch_name, commit_message
120
- )
121
- else:
122
- logger.warning(
123
- f"{project}: no git operations to do. No files modified"
124
- )
125
-
126
- logger.warning(f"Skipped projects: {skipped}")
127
-
128
- def _get_all_templated_projects(self, template_type):
129
- if template_type == "drone":
130
- values_path = self._drone_values_path
131
- elif template_type == "deploy_scripts":
132
- values_path = self._deploy_scripts_values_path
133
-
134
- return [
135
- f.replace(".yml", "")
136
- for f in listdir(values_path)
137
- if isfile(join(values_path, f))
138
- ]
139
-
140
- def _git_operations(self, git, autobranch, autopush, branch_name, message):
141
- if autobranch:
142
- logger.info(f"{git.get_repo()}: creating branch {branch_name}")
143
- git.checkout(branch_name, new=True)
144
- git.add()
145
- git.commit(message)
146
- git.push(branch_name)
147
- pr = self._github.create_pr(git.get_repo(), branch_name, message)
148
- logger.info(f"Pull request with number {pr.number} created! {pr.html_url}")
149
- elif autopush:
150
- if git.current_branch_name() != "master":
151
- git.add()
152
- git.commit(message)
153
- git.push(branch_name)
154
- else:
155
- logger.warning(
156
- f"{git.get_repo()} is on master, skipping automatic push"
157
- )
158
-
159
- def _generate_drone(self, project, values_file, env):
160
- values = yaml.safe_load(open(values_file, encoding="utf-8"))
161
- template = env.get_template(f"templates/drone/{values['template']}.j2")
162
- rendered = template.render(values)
163
-
164
- _write_on_repo(
165
- os.path.join(self._config.user["projects_home"], project),
166
- rendered,
167
- ".drone.yml",
168
- )
169
- drone = DroneHandler(self._config, self._tokens, repo=project)
170
- drone.fmt()
171
- drone.validate()
172
- drone.sign()
173
-
174
-
175
- def _configure_branch(git, template_type, autobranch, autopush):
176
- logger.info(f"{git.get_repo()}: Synchronizing the repo with the remote")
177
- if autobranch:
178
- git.sync() # fa anche checkout su master
179
- return f"t/autogenerated_update_{template_type}_{NOW}"
180
-
181
- current_branch = git.current_branch_name()
182
- if autopush and git.remote_branch_exists(current_branch):
183
- git.fetch()
184
- git.pull()
185
-
186
- return current_branch
187
-
188
-
189
- def _generate_build_script(project_path, values_file, env):
190
- file_name = "build"
191
- values = yaml.safe_load(open(values_file, encoding="utf-8"))
192
-
193
- val = {**values["info"], **values["build"]}
194
- template = env.get_template(f"templates/scripts/build/{val['template']}")
195
- rendered = template.render(val)
196
-
197
- _write_on_repo(os.path.join(project_path, "deploy"), rendered, file_name)
198
- _format_script(f"{project_path}/deploy/{file_name}")
199
- _validate_script(f"{project_path}/deploy/{file_name}")
200
-
201
-
202
- def _generate_deploy_script(project_path, values_file, env):
203
- values = yaml.safe_load(open(values_file, encoding="utf-8"))
204
-
205
- for country, country_props in values["deploy"].items():
206
- val = {**values["info"], **country_props}
207
- template = env.get_template("templates/scripts/deploy/base.j2")
208
- rendered = template.render(val)
209
- file_name = f"deploy-{country}"
210
- _write_on_repo(os.path.join(project_path, "deploy"), rendered, file_name)
211
- _format_script(f"{project_path}/deploy/{file_name}")
212
- _validate_script(f"{project_path}/deploy/{file_name}")
213
-
214
-
215
- def _write_on_repo(path, rendered, file_name):
216
- if not os.path.exists(path):
217
- os.mkdir(path, mode=0o755)
218
-
219
- with open(os.path.join(path, file_name), "w", encoding="utf-8") as fd:
220
- fd.write(rendered)
221
-
222
-
223
- def _format_script(filepath):
224
- subprocess.run(["shfmt", "-i", "2", "-l", "-w", filepath], check=True)
225
-
226
-
227
- def _validate_script(filepath):
228
- _make_executable(filepath)
229
- subprocess.run(["shellcheck", filepath], check=True)
230
-
231
-
232
- def _make_executable(filepath):
233
- st = os.stat(filepath)
234
- os.chmod(filepath, st.st_mode | stat.S_IEXEC)
235
-
236
-
237
- def to_yaml(d, indent=10):
238
- return yaml.dump(d, indent=indent)
suite_py/commands/id.py DELETED
@@ -1,60 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- import re
3
-
4
- from pptree import Node, print_tree
5
-
6
- from suite_py.lib import logger, metrics
7
- from suite_py.lib.handler.aws_handler import Aws
8
-
9
-
10
- class ID:
11
- def __init__(self, project, config, env):
12
- self._project = project
13
- self._env = env
14
- self._aws = Aws(config)
15
-
16
- @metrics.command("id")
17
- def run(self):
18
-
19
- clusters_names = self._aws.get_ecs_clusters(self._env)
20
- n_services = Node("services")
21
-
22
- projects = {"prima": ["web", "consumer-api"], "ab_normal": ["abnormal"]}
23
- project_names = projects.get(self._project, [self._project])
24
-
25
- for cluster_name in clusters_names:
26
-
27
- services = []
28
- all_services = self._aws.get_ecs_services(cluster_name)
29
-
30
- for service in all_services:
31
- if service["status"] == "ACTIVE":
32
- for prj in project_names:
33
- if prj in service["serviceName"]:
34
- services.append(service["serviceName"])
35
-
36
- for service in services:
37
- container_instances = []
38
- container_instances = (
39
- self._aws.get_container_instances_arn_from_service(
40
- cluster_name, service
41
- )
42
- )
43
- if container_instances:
44
- ids = self._aws.get_ids_from_container_instances(
45
- cluster_name, container_instances
46
- )
47
-
48
- m = re.search(f"ecs-task-.*-{self._env}-ECSService(.*)-.*", service)
49
- if m:
50
- n_service = Node(m.group(1), n_services)
51
- for _id in ids:
52
- Node(_id, n_service)
53
-
54
- if n_services.children:
55
- print_tree(n_services, horizontal=True)
56
- else:
57
- logger.info(
58
- f"No active tasks for {self._project} in environment {self._env}"
59
- )
60
- logger.info("Done!")
suite_py/commands/ip.py DELETED
@@ -1,68 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- import os
3
- import re
4
-
5
- from pptree import Node, print_tree
6
-
7
- from suite_py.lib import logger, metrics
8
- from suite_py.lib.handler.aws_handler import Aws
9
-
10
-
11
- class IP:
12
- def __init__(self, project, config, env):
13
- self._project = project
14
- self._env = env
15
- self._aws = Aws(config)
16
-
17
- @metrics.command("ip")
18
- def run(self):
19
-
20
- clusters_names = self._aws.get_ecs_clusters(self._env)
21
- n_services = Node("services")
22
-
23
- projects = {"prima": ["web", "consumer-api"], "ab_normal": ["abnormal"]}
24
- project_names = projects.get(self._project, [self._project])
25
-
26
- for cluster_name in clusters_names:
27
-
28
- services = []
29
- all_services = self._aws.get_ecs_services(cluster_name)
30
-
31
- for service in all_services:
32
- if service["status"] == "ACTIVE":
33
- for prj in project_names:
34
- if prj in service["serviceName"]:
35
- services.append(service["serviceName"])
36
-
37
- for service in services:
38
- container_instances = []
39
- container_instances = (
40
- self._aws.get_container_instances_arn_from_service(
41
- cluster_name, service
42
- )
43
- )
44
- if container_instances:
45
- ips = self._aws.get_ips_from_container_instances(
46
- cluster_name, container_instances
47
- )
48
-
49
- m = re.search(f"ecs-task-.*-{self._env}-ECSService(.*)-.*", service)
50
- if m:
51
- n_service = Node(m.group(1), n_services)
52
- for ip in ips:
53
- Node(ip, n_service)
54
-
55
- if self._env == "staging" and os.path.isfile("./deploy/values/staging.yml"):
56
- logger.info(
57
- "This project has been migrated on k8s, command is no longer available."
58
- )
59
- logger.info(
60
- "Please use k8s to open a shell to a staging pod, see: https://www.notion.so/K8s-tips-2f8b3fed72334362af0de0298845756d"
61
- )
62
- elif n_services.children:
63
- print_tree(n_services, horizontal=True)
64
- else:
65
- logger.info(
66
- f"No active tasks for {self._project} in environment {self._env}"
67
- )
68
- logger.info("Done!")
@@ -1,204 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- import json
3
- import os
4
- import sys
5
-
6
- import yaml
7
-
8
- from suite_py.lib import logger, metrics
9
- from suite_py.lib.handler import prompt_utils
10
- from suite_py.lib.handler.vault_handler import VaultHandler
11
-
12
- profiles_mapping = {
13
- "common": {
14
- "stack_name_pattern": "{service}-platform-cluster-{env}-serviceaccount-iamrole",
15
- "logical_id": "ServiceAccountIamRole",
16
- "developer_role": "arn:aws:iam::595659439703:role/aws-reserved/sso.amazonaws.com/eu-west-1/AWSReservedSSO_ItalyDeveloperPlatform_cb5de4d001016cc4",
17
- },
18
- "it": {
19
- "stack_name_pattern": "ecs-roles-{service}-{env}",
20
- "logical_id": "ECSTaskRole",
21
- "developer_role": "arn:aws:iam::001575623345:role/AllowNonProductionBiscuitDecryption",
22
- },
23
- "uk": {
24
- "stack_name_pattern": "{service}-main-{env}-serviceaccount-iamrole",
25
- "logical_id": "ServiceAccountIamRole",
26
- "developer_role": "arn:aws:iam::815911007681:role/aws-reserved/sso.amazonaws.com/eu-west-1/AWSReservedSSO_UnitedKingdomDeveloper_f578b0b2b98ea593",
27
- },
28
- "es": {
29
- "stack_name_pattern": "{service}-main-{env}-serviceaccount-iamrole",
30
- "logical_id": "ServiceAccountIamRole",
31
- "developer_role": "arn:aws:iam::199959896441:role/aws-reserved/sso.amazonaws.com/eu-west-1/AWSReservedSSO_SpainDeveloper_acf53aa98cfc0ac4",
32
- },
33
- }
34
-
35
-
36
- class Secret:
37
- def __init__(self, project, config, action, base_profile, secret_file):
38
- self._project = project
39
- self._config = config
40
- self._path = os.path.join(config.user["projects_home"], project)
41
- self._vault = VaultHandler(project, config)
42
- self._action = action
43
- self._base_profile = base_profile
44
- self._secret_file = secret_file
45
-
46
- @metrics.command("secret")
47
- def run(self):
48
- if self._base_profile is not None:
49
- self._config.vault["base_secret_profile"] = self._base_profile
50
-
51
- if not self._secret_file:
52
- self._secret_file = "config/secrets.yml"
53
-
54
- if self._action == "create":
55
- self._create_new_secret()
56
- return
57
-
58
- if self._action == "grant":
59
- secrets = self._get_available_secrets()
60
- secret = prompt_utils.ask_choices("Select secret: ", secrets)
61
- self._set_grant_on_secret(secret)
62
- return
63
-
64
- logger.error(
65
- "You have to specify what to do! See available flags with suite-py secret --help"
66
- )
67
-
68
- def _create_new_secret(self):
69
- secret = prompt_utils.ask_questions_input("Enter secret name: ")
70
- value = prompt_utils.ask_questions_input("Enter secret value: ")
71
- e = self._vault.exec(
72
- self._config.vault["base_secret_profile"],
73
- f"biscuit put -f {self._secret_file} -- {secret} {value}",
74
- )
75
- if e.returncode != 0:
76
- logger.error("An error occurred.")
77
- sys.exit(1)
78
-
79
- if prompt_utils.ask_confirm(f"Do you want to grant permissions for {secret}?"):
80
- self._set_grant_on_secret(secret)
81
-
82
- def _set_grant_on_secret(self, secret_name):
83
- environment = self._ask_environment()
84
- profiles = self._ask_profile()
85
- for profile in profiles:
86
- mapping = profiles_mapping[profile]
87
- stack_name = (
88
- mapping["stack_name_pattern"]
89
- .replace("{service}", self._project)
90
- .replace("{env}", environment)
91
- )
92
- # Get stack resource info
93
- logger.info(
94
- f"Obtaining ARN for {mapping['logical_id']} from stack {stack_name}..."
95
- )
96
- stack_resource = self._get_stack_resource(
97
- profile, stack_name, mapping["logical_id"]
98
- )
99
- if stack_resource is None:
100
- iam_role_arn = self._manual_iam_arn()
101
- else:
102
- iam_role_name = stack_resource["StackResourceDetail"][
103
- "PhysicalResourceId"
104
- ]
105
- # Get IAM Role ARN
106
- iam_role = self._get_iam_role(profile, iam_role_name)
107
- iam_role_arn = iam_role["Role"]["Arn"]
108
- if iam_role_arn is None:
109
- iam_role_arn = self._manual_iam_arn()
110
- logger.info(f"ARN found for profile {profile}: {iam_role_arn}")
111
-
112
- if iam_role_arn is None:
113
- continue
114
-
115
- logger.debug(
116
- f"---\nGranting permissions on {self._project}/{secret_name}\nEnvironment: {environment}\nUsing vault profile: {profile}\nIAM Role ARN: {iam_role_arn}\n---"
117
- )
118
- if not prompt_utils.ask_confirm("Do you want to continue?", default=True):
119
- continue
120
-
121
- # Grant decrypt permissions to task
122
- logger.info("Granting permissions to task...")
123
- e = self._vault.exec(
124
- self._config.vault["base_secret_profile"],
125
- f"biscuit kms grants create --grantee-principal {iam_role_arn} -f {self._secret_file} {secret_name}",
126
- )
127
- if e.returncode != 0:
128
- logger.error("An error occurred.")
129
- continue
130
-
131
- # Grant decrypt permissions to developers group
132
- if environment in ["staging", "qa"]:
133
- logger.info("Granting permissions to devs...")
134
- e = self._vault.exec(
135
- self._config.vault["base_secret_profile"],
136
- f"biscuit kms grants create --grantee-principal {mapping['developer_role']} -f {self._secret_file} {secret_name}",
137
- )
138
- if e.returncode != 0:
139
- logger.error("An error occurred.")
140
- continue
141
-
142
- logger.info("Ta-da! Permissions granted.")
143
-
144
- def _get_available_secrets(self):
145
- try:
146
- secrets = []
147
- with open(f"{self._path}/{self._secret_file}", encoding="utf-8") as s:
148
- yaml_secrets = yaml.load(s, Loader=yaml.FullLoader)
149
- secrets = [
150
- key for key, values in yaml_secrets.items() if key != "_keys"
151
- ]
152
-
153
- return secrets
154
- except FileNotFoundError:
155
- logger.error(f"Can't load secrets from {self._secret_file}.")
156
- sys.exit(1)
157
-
158
- def _get_stack_resource(self, profile, stack_name, logical_id):
159
- e = self._vault.exec(
160
- profile,
161
- f"aws cloudformation describe-stack-resource --stack-name {stack_name} --logical-resource-id {logical_id}",
162
- )
163
- command_result = e.stdout.read()
164
- if e.returncode != 0:
165
- logger.warning("An error occurred trying to obtain stack. Skipping...")
166
- return None
167
- stack_info = command_result.decode("utf8").replace("'", '"')
168
- data = json.loads(stack_info)
169
- return data
170
-
171
- def _get_iam_role(self, profile, iam_role_name):
172
- e = self._vault.exec(
173
- profile,
174
- f"aws iam get-role --role-name {iam_role_name}",
175
- additional_args="--no-session",
176
- )
177
- command_result = e.stdout.read()
178
- if e.returncode != 0:
179
- logger.warning(
180
- "An error occurred trying to obtain IAM Role details. Skipping..."
181
- )
182
- return None
183
- stack_info = command_result.decode("utf8").replace("'", '"')
184
- data = json.loads(stack_info)
185
- return data
186
-
187
- def _manual_iam_arn(self):
188
- arn = prompt_utils.ask_questions_input(
189
- "No associated IAM Role ARN found. Insert it manually or leave blank to skip",
190
- default_text="",
191
- )
192
- if arn == "":
193
- return None
194
- return arn
195
-
196
- def _ask_environment(self):
197
- return prompt_utils.ask_choices(
198
- "Select an environment:", ["production", "staging", "qa"]
199
- )
200
-
201
- def _ask_profile(self):
202
- return prompt_utils.ask_multiple_choices(
203
- "Select vault profile(s):", self._config.vault["profiles"]
204
- )