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,159 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- import re
3
- import sys
4
-
5
- from halo import Halo
6
- from kubernetes import client, config
7
- from rich.console import Console
8
- from rich.table import Table
9
-
10
- from suite_py.lib import logger, metrics
11
- from suite_py.lib.handler import prompt_utils
12
-
13
-
14
- class Aggregator:
15
- def __init__(self, captainhook, command):
16
- self._captainhook = captainhook
17
- self._command = command
18
-
19
- @metrics.command("aggregator")
20
- def run(self):
21
- if self._command == "list":
22
- self._list_aggregators()
23
- return
24
-
25
- if self._command == "change":
26
- aggregator = self._select_aggregator()
27
- address = prompt_utils.ask_questions_input(
28
- "Insert QA url or press enter to set staging URL: ",
29
- default_text="staging.prima.it",
30
- )
31
-
32
- if address.startswith("http"):
33
- address = re.sub(r"https?://?", "", address)
34
-
35
- split_addr = re.split(r"([a-z]+)\-([a-z0-9]+)", address)
36
-
37
- change_request = self._captainhook.change_aggregator(
38
- aggregator["name"], address
39
- )
40
-
41
- self._handle_captainhook_response(
42
- change_request, aggregator["name"], address
43
- )
44
-
45
- if "www" in split_addr:
46
- split_addr[1] = "prima"
47
- self._update_k8s_ingress_route(aggregator, split_addr[1], split_addr[2])
48
- elif "evvivass" in split_addr:
49
- self._update_k8s_ingress_route(aggregator, split_addr[1], split_addr[2])
50
- else:
51
- return
52
-
53
- def _handle_captainhook_response(self, request, aggregator, address):
54
- if request.status_code == 200:
55
- change_request = request.json()
56
- if change_request["success"]:
57
- logger.info(f"CNAME updated! Now {aggregator} is pointing to {address}")
58
- else:
59
- cases = {
60
- "cloudflare_error": "Error during Cloudflare invocation.",
61
- "unknown_dns_record": "Impossible to find DNS record associated with aggregator.",
62
- "unknown_aggregator": "Aggregator not found.",
63
- "invalid_qa_address": "The QA address does not meet the requirements.",
64
- }
65
- logger.error(cases.get(change_request["error"], "unknown error"))
66
- sys.exit(-1)
67
- else:
68
- logger.error("An error has occurred on Captainhook.")
69
- sys.exit(-1)
70
-
71
- def _select_aggregator(self):
72
- with Halo(text="Loading aggregators...", spinner="dots", color="magenta"):
73
- choices = [
74
- {"name": agg["name"], "value": agg}
75
- for agg in self._captainhook.aggregators().json()
76
- ]
77
- if choices:
78
- choices.sort(key=lambda x: x["name"])
79
- return prompt_utils.ask_choices("Select aggregator: ", choices)
80
-
81
- logger.error("There are no aggregators on Captainhook.")
82
- sys.exit(-1)
83
-
84
- def _list_aggregators(self):
85
- with Halo(text="Loading...", spinner="dots", color="magenta"):
86
- aggregators = self._captainhook.aggregators()
87
-
88
- if aggregators.status_code != 200:
89
- logger.error("Unable to retrieve the list of aggregators.")
90
- return
91
-
92
- console = Console()
93
-
94
- aggregators_table = Table()
95
- aggregators_table.add_column("Name", style="green")
96
- aggregators_table.add_column("Address", style="white")
97
-
98
- for a in aggregators.json():
99
- aggregators_table.add_row(a["name"], a["content"])
100
-
101
- logger.info("Available aggregators:")
102
- console.print(aggregators_table)
103
-
104
- def _update_k8s_ingress_route(self, aggregator, service_name, namespace):
105
- try:
106
- config.load_kube_config()
107
- except Exception:
108
- logger.error(
109
- "\n\nYou need to authenticate, run the following command:\n$ aws eks update-kubeconfig --name main-qa\n\n"
110
- )
111
- sys.exit(-1)
112
-
113
- api = client.CustomObjectsApi()
114
- res = {
115
- "apiVersion": "traefik.containo.us/v1alpha1",
116
- "kind": "IngressRoute",
117
- "metadata": {
118
- "name": f"{aggregator['name']}",
119
- "namespace": f"{namespace}",
120
- "annotations": {
121
- "traefik.ingress.kubernetes.io/router.tls": "true",
122
- },
123
- "labels": {
124
- "com.prima.environment": "qa",
125
- "com.prima.country": "it",
126
- "app.kubernetes.io/name": f"{aggregator['name']}",
127
- },
128
- },
129
- "spec": {
130
- "entryPoints": ["websecure"],
131
- "routes": [
132
- {
133
- "kind": "Rule",
134
- "match": f"Host(`{aggregator['address']}{aggregator['domain']}`)",
135
- "services": [{"name": f"{service_name}", "port": 80}],
136
- }
137
- ],
138
- },
139
- }
140
-
141
- try:
142
- api.create_namespaced_custom_object(
143
- group="traefik.containo.us",
144
- version="v1alpha1",
145
- namespace=namespace,
146
- plural="ingressroutes",
147
- body=res,
148
- )
149
- logger.info(f"Aggregator {aggregator['name']} created!")
150
- except Exception:
151
- api.patch_namespaced_custom_object(
152
- group="traefik.containo.us",
153
- version="v1alpha1",
154
- namespace=namespace,
155
- plural="ingressroutes",
156
- name=aggregator["name"],
157
- body=res,
158
- )
159
- logger.info(f"Aggregator {aggregator['name']} updated!")
@@ -1,215 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- import re
3
- import sys
4
- import textwrap
5
-
6
- import semver
7
-
8
- from suite_py.lib import logger, metrics
9
- from suite_py.lib.handler import prompt_utils
10
- from suite_py.lib.handler.drone_handler import DroneHandler
11
- from suite_py.lib.handler.git_handler import GitHandler
12
- from suite_py.lib.handler.github_handler import GithubHandler
13
-
14
-
15
- class BatchJob:
16
- def __init__(
17
- self, project, config, tokens, environment, cpu_request, memory_request
18
- ):
19
- self._project = project
20
- self._config = config
21
- self._tokens = tokens
22
- self._environment = environment
23
- self._cpu_request = cpu_request
24
- self._memory_request = memory_request
25
- self._git = GitHandler(project, config)
26
- self._github = GithubHandler(tokens)
27
- self._drone = DroneHandler(config, tokens, repo=project)
28
- self._countries = _parse_available_countries(self._drone)
29
- self._repo = self._github.get_repo(project)
30
-
31
- @metrics.command("batch-job")
32
- def run(self):
33
- country = prompt_utils.ask_choices(
34
- "Which country do you want to run the job on?",
35
- self._countries,
36
- )
37
-
38
- if not _has_job_pipeline(self._drone, country, self._environment):
39
- logger.error(
40
- "There is no job pipeline for the specified country/environment"
41
- )
42
- sys.exit(1)
43
-
44
- command = prompt_utils.ask_questions_input(
45
- "What command do you want to execute?", f"/app/bin/{self._project} "
46
- )
47
-
48
- self._start_job(country, command)
49
-
50
- def _start_job(self, country, command):
51
- promotion = {}
52
-
53
- if self._environment == "staging":
54
- build = self._get_latest_master_build()
55
-
56
- self._ask_confirm(
57
- country,
58
- command,
59
- f'Current master: {build["message"]} (#{build["number"]})',
60
- )
61
-
62
- promotion = self._drone.promote_staging(
63
- build["number"],
64
- f"job-{country}-{self._environment}",
65
- f"BATCH_COMMAND={command}&JOB_CPU={self._cpu_request}&JOB_MEMORY={self._memory_request}",
66
- )
67
- elif self._environment == "production":
68
- version = self._get_latest_tag(country)
69
-
70
- self._ask_confirm(country, command, f"Current tag: {version}")
71
-
72
- promotion = self._drone.promote_production(
73
- version,
74
- f"job-{country}-{self._environment}",
75
- f"DRONE_TAG={version}&BATCH_COMMAND={command}&JOB_CPU={self._cpu_request}&JOB_MEMORY={self._memory_request}",
76
- )
77
-
78
- if "number" not in promotion:
79
- logger.warning(f"Unable to promote drone build. Response: {promotion}")
80
- return
81
-
82
- logger.info("Drone build started successfully!")
83
- logger.info(
84
- f"You can follow the build status here: {self._drone.get_build_url(promotion['number'])}"
85
- )
86
-
87
- def _get_latest_master_build(self):
88
- try:
89
- builds = self._drone.get_builds_from_branch("master")
90
-
91
- latest_build = None
92
- non_green_builds = []
93
- for b in builds:
94
- if b["status"] == "success":
95
- if latest_build is None:
96
- latest_build = b
97
- break
98
-
99
- non_green_builds.append(b)
100
-
101
- if len(non_green_builds) > 0:
102
- logger.warning(
103
- "There are recent builds on master still running or failed"
104
- )
105
- for b in non_green_builds:
106
- logger.warning(
107
- f'* {b["message"]} (Status: {b["status"]}, Number: #{b["number"]})'
108
- )
109
-
110
- if not latest_build:
111
- logger.error("Unable to find latest build on master")
112
- sys.exit(255)
113
-
114
- return latest_build
115
-
116
- except Exception as e:
117
- print(e)
118
- logger.error(
119
- "An error has occurred retrieving current master version during batch job.\nPlease ask #team-platform-operations for help."
120
- )
121
- sys.exit(255)
122
-
123
- def _get_latest_tag(self, country):
124
- try:
125
- logger.info("Retrieving latest version, this may take some time...")
126
- # get latest 10 tags
127
- tags = self._github.get_tags(self._project)
128
- # exclude tags that don't match semver notation
129
- semver_tags = [t for t in tags if semver.VersionInfo.isvalid(t.name)][0:9]
130
-
131
- builds = []
132
- for tag in semver_tags:
133
- for b in self._drone.get_builds_from_tag(tag.name):
134
- if (
135
- b["event"] == "promote"
136
- and b["status"] == "success"
137
- and b["deploy_to"] == f"deploy-{country}-production"
138
- and "params" in b
139
- and "DRONE_TAG" in b["params"]
140
- ):
141
- builds.append(b)
142
-
143
- if not builds:
144
- # may end up here if there isn't any successfull build for latest 10 tags
145
- logger.error(
146
- "An error has occurred retrieving current version during rollback.\nPlease ask #team-platform-operations for help."
147
- )
148
- sys.exit(255)
149
-
150
- # get latest build using build number as key
151
- current_build = max(builds, key=lambda x: x["number"])
152
-
153
- if not current_build:
154
- logger.error(
155
- f"Unable to determine current version for country {country}"
156
- )
157
- sys.exit(255)
158
-
159
- v = semver.VersionInfo.parse(current_build["params"]["DRONE_TAG"])
160
- logger.info(f"Current version for country {country}: {v}")
161
-
162
- return v
163
- except Exception as e:
164
- print(e)
165
- logger.error(
166
- "An error has occurred retrieving current version during batch job.\nPlease ask #team-platform-operations for help."
167
- )
168
- sys.exit(255)
169
-
170
- def _ask_confirm(self, country, command, message):
171
- logger.info(
172
- textwrap.dedent(
173
- f"""
174
- You're about to run a batch job on project {self._project}, for env {self._environment}, in country {country}.
175
- {message}
176
- Command: {command}
177
- """
178
- )
179
- )
180
-
181
- if not prompt_utils.ask_confirm("Do you confirm?", default=False):
182
- sys.exit(0)
183
-
184
-
185
- def _parse_available_countries(drone):
186
- pipelines = drone.parse_yaml()
187
-
188
- if pipelines is None:
189
- logger.error("The file .drone.yml was not found. Unable to continue.")
190
- sys.exit(1)
191
-
192
- countries = []
193
- REGEX = re.compile(r"deploy-([a-z]+)-.*")
194
- for pipeline in pipelines:
195
- if "name" in pipeline:
196
- c = REGEX.findall(pipeline["name"])
197
- if len(c) > 0 and c[0] is not None and c[0] not in countries:
198
- countries.append(c[0])
199
-
200
- return countries
201
-
202
-
203
- def _has_job_pipeline(drone, country, environment):
204
- pipelines = drone.parse_yaml()
205
-
206
- if pipelines is None:
207
- logger.error("The file .drone.yml was not found. Unable to continue.")
208
- sys.exit(1)
209
-
210
- job_pipeline = f"job-{country}-{environment}"
211
- for pipeline in pipelines:
212
- if "name" in pipeline and pipeline["name"] == job_pipeline:
213
- return True
214
-
215
- return False
@@ -1,207 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- import re
3
- import sys
4
-
5
- from suite_py.commands import common
6
- from suite_py.commands.release import _parse_available_countries
7
- from suite_py.lib import logger, metrics
8
- from suite_py.lib.handler import git_handler as git
9
- from suite_py.lib.handler import prompt_utils
10
- from suite_py.lib.handler.changelog_handler import ChangelogHandler
11
- from suite_py.lib.handler.drone_handler import DroneHandler
12
- from suite_py.lib.handler.git_handler import GitHandler
13
- from suite_py.lib.handler.github_handler import GithubHandler
14
- from suite_py.lib.handler.version_handler import DEFAULT_VERSION, VersionHandler
15
- from suite_py.lib.handler.youtrack_handler import YoutrackHandler
16
-
17
-
18
- class Deploy:
19
- # pylint: disable=too-many-instance-attributes
20
- def __init__(self, project, captainhook, config, tokens):
21
- self._project = project
22
- self._config = config
23
- self._youtrack = YoutrackHandler(config, tokens)
24
- self._captainhook = captainhook
25
- self._changelog_handler = ChangelogHandler()
26
- self._github = GithubHandler(tokens)
27
- self._repo = self._github.get_repo(project)
28
- self._git = GitHandler(project, config)
29
- self._drone = DroneHandler(config, tokens, repo=project)
30
- self._countries = _parse_available_countries(self._drone)
31
- self._version = VersionHandler(self._repo, self._git, self._github)
32
-
33
- @metrics.command("deploy")
34
- def run(self):
35
- self._stop_if_prod_locked()
36
-
37
- self._git.fetch()
38
-
39
- if len(self._countries) > 0:
40
- logger.error(
41
- "Deploy command cannot be used on this project. Try to run `suite-py release` instead."
42
- )
43
- sys.exit(1)
44
-
45
- current_version = self._version.get_latest_version()
46
-
47
- if current_version != "":
48
- logger.info(f"The current release is {current_version}")
49
- commits = self._github.get_commits_since_release(
50
- self._repo, current_version
51
- )
52
-
53
- _check_migrations_deploy(commits)
54
-
55
- message = "\n".join(
56
- [
57
- "* "
58
- + c.commit.message.splitlines()[0]
59
- + " by "
60
- + c.commit.author.name
61
- for c in commits
62
- ]
63
- )
64
-
65
- logger.info(f"\nCommits list:\n{message}\n")
66
-
67
- if not prompt_utils.ask_confirm("Do you want to continue?"):
68
- sys.exit()
69
-
70
- new_version = self._version.select_new_version(
71
- current_version, allow_prerelease=True, allow_custom_version=True
72
- )
73
-
74
- else:
75
- # Se non viene trovata la release e non ci sono tag, viene saltato il check delle migrations e l'update delle card su youtrack
76
- logger.warning(
77
- f"No tags found, I'm about to push the tag {DEFAULT_VERSION}"
78
- )
79
- if not prompt_utils.ask_confirm(
80
- "Are you sure you want to continue?", default=False
81
- ):
82
- sys.exit()
83
- new_version = DEFAULT_VERSION
84
- message = f"First release with tag {new_version}"
85
-
86
- if self._changelog_handler.changelog_exists():
87
- (
88
- latest_tag,
89
- latest_entry,
90
- ) = self._changelog_handler.get_latest_entry_with_tag()
91
-
92
- if latest_tag != new_version:
93
- if not prompt_utils.ask_confirm(
94
- "You didn't update your changelog, are you sure you want to proceed?"
95
- ):
96
- sys.exit()
97
- else:
98
- message = f"{latest_entry}\n\n# Commits\n\n{message}"
99
-
100
- message = common.ask_for_release_description(message)
101
-
102
- self._create_release(new_version, message)
103
- self._manage_youtrack_card(commits, new_version)
104
-
105
- def _stop_if_prod_locked(self):
106
- request = self._captainhook.status(self._project, "production")
107
- if request.status_code != 200:
108
- logger.error("Unable to determine lock status on master.")
109
- sys.exit(-1)
110
-
111
- request_object = request.json()
112
- if request_object["locked"]:
113
- logger.error(
114
- f"The project is locked in production by {request_object['by']}. Unable to continue."
115
- )
116
- sys.exit(-1)
117
-
118
- def _create_release(self, new_version, message):
119
- new_release = self._repo.create_git_release(
120
- new_version,
121
- new_version,
122
- self._youtrack.replace_card_names_with_md_links(message),
123
- )
124
- if new_release:
125
- logger.info(f"The release has been created! Link: {new_release.html_url}")
126
-
127
- build_number = self._drone.get_build_number_from_tag(new_version)
128
- if build_number:
129
- drone_url = self._drone.get_build_url(build_number)
130
- logger.info(
131
- f"You can follow the deployment in production here: {drone_url}"
132
- )
133
-
134
- def _manage_youtrack_card(self, commits, new_version):
135
- release_state = self._config.youtrack["release_state"]
136
-
137
- issue_ids = self._youtrack.get_issue_ids(commits)
138
-
139
- if len(issue_ids) > 0:
140
- update_youtrack_state = prompt_utils.ask_confirm(
141
- f"Do you want to move the associated cards to {release_state} state?",
142
- default=False,
143
- )
144
-
145
- for issue_id in issue_ids:
146
- try:
147
- self._youtrack.comment(
148
- issue_id,
149
- f"Deploy in production of {self._project} done with the release {new_version}",
150
- )
151
- if update_youtrack_state:
152
- self._youtrack.update_state(issue_id, release_state)
153
- logger.info(f"{issue_id} moved to {release_state}")
154
- except Exception:
155
- logger.warning(
156
- f"An error occurred while moving the card {issue_id} to {release_state}"
157
- )
158
- repos_status = self._get_repos_status_from_issue(issue_id)
159
- if all(r["deployed"] for r in repos_status.values()):
160
- try:
161
- self._youtrack.update_deployed_field(issue_id)
162
- logger.info("Custom field Deployed updated on YouTrack")
163
- except Exception:
164
- logger.warning(
165
- "An error occurred while updating the custom field Deployed"
166
- )
167
-
168
- def _get_repos_status_from_issue(self, issue_id):
169
- regex_pr = r"^PR .* -> https:\/\/github\.com\/primait\/(.*)\/pull\/([0-9]*)$"
170
- regex_deploy = r"^Deploy in production of (.*) done with the release"
171
- comments = self._youtrack.get_comments(issue_id)
172
- repos_status = {}
173
-
174
- for c in comments:
175
- m = re.match(regex_pr, c["text"])
176
- if m:
177
- project = m.group(1)
178
- pr_number = int(m.group(2))
179
- repos_status[project] = {}
180
- repos_status[project]["pr"] = pr_number
181
- repos_status[project]["deployed"] = False
182
- m = re.match(regex_deploy, c["text"])
183
- if m:
184
- project = m.group(1)
185
- try:
186
- repos_status[project]["deployed"] = True
187
- except Exception:
188
- pass
189
- return repos_status
190
-
191
-
192
- def _check_migrations_deploy(commits):
193
- if not commits:
194
- logger.error("ERROR: no commit found")
195
- sys.exit(-1)
196
- elif len(commits) == 1:
197
- files_changed = git.files_changed_between_commits("--raw", f"{commits[0].sha}~")
198
- else:
199
- files_changed = git.files_changed_between_commits(
200
- f"{commits[-1].sha}~", commits[0].sha
201
- )
202
- if git.migrations_found(files_changed):
203
- logger.warning("WARNING: migrations detected in the code")
204
- if not prompt_utils.ask_confirm(
205
- "Are you sure you want to continue?", default=False
206
- ):
207
- sys.exit()
@@ -1,91 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- import sys
3
-
4
- from halo import Halo
5
-
6
- from suite_py.lib import logger, metrics
7
- from suite_py.lib.handler import prompt_utils
8
- from suite_py.lib.handler.drone_handler import DroneHandler
9
- from suite_py.lib.handler.git_handler import GitHandler
10
- from suite_py.lib.handler.github_handler import GithubHandler
11
-
12
-
13
- class Docker:
14
- # pylint: disable=too-many-instance-attributes
15
- def __init__(self, action, project, config, tokens, flags=None):
16
- self._action = action
17
- self._project = project
18
- self._flags = flags
19
- self._config = config
20
- self._tokens = tokens
21
- self._github = GithubHandler(tokens)
22
- self._repo = self._github.get_repo(project)
23
- self._git = GitHandler(project, config)
24
- self._drone = DroneHandler(config, tokens, repo=project)
25
-
26
- @metrics.command("docker")
27
- def run(self):
28
- if self._project != "docker":
29
- logger.error("`suite-py docker` must run inside docker repository.")
30
- sys.exit(-1)
31
-
32
- self._git.fetch()
33
-
34
- pipelines = self._drone.parse_yaml()
35
-
36
- if pipelines is None:
37
- logger.error("The file .drone.yml not found.")
38
- sys.exit(1)
39
-
40
- images = []
41
- for pipeline in pipelines:
42
- if "trigger" not in pipeline or "ref" not in pipeline["trigger"]:
43
- continue
44
-
45
- trigger_tags = pipeline["trigger"]["ref"]
46
-
47
- for tag in trigger_tags:
48
- images.append(tag.replace("refs/tags/", ""))
49
-
50
- image = prompt_utils.ask_choices(
51
- "Select an image",
52
- images,
53
- )
54
- versions = self._get_versions(image)
55
-
56
- if self._action == "release":
57
- print_versions(versions)
58
- new_version = self._ask_new_version(image)
59
- self._create_new_version(new_version)
60
-
61
- elif self._action == "versions":
62
- print_versions(versions)
63
-
64
- def _create_new_version(self, version):
65
- if prompt_utils.ask_confirm(f"Tag {version} will be deployed. Confirm?"):
66
- self._git.tag(version, version)
67
- self._git.push(version)
68
-
69
- def _ask_new_version(self, image):
70
- new = prompt_utils.ask_questions_input(
71
- "Write new version to be deployed (eg: 11.3-1):"
72
- )
73
- if new[0] == "v":
74
- logger.error("Insert new version without `v`")
75
- sys.exit(-1)
76
- return f'{image.replace("*", "")}{new}'
77
-
78
- def _get_versions(self, image):
79
- versions = []
80
- with Halo(text="Retrieving tags...", spinner="dots", color="magenta"):
81
- tags = self._github.get_tags("docker")
82
- for tag in tags:
83
- if image.replace("*", "") in tag.name:
84
- versions.append(tag)
85
- return versions
86
-
87
-
88
- def print_versions(versions):
89
- message = "\n".join(["* " + v.name for v in versions])
90
-
91
- logger.info(f"\nVersions list:\n{message}\n")