suite-py 1.41.2__py3-none-any.whl → 1.41.4__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.
- suite_py/__version__.py +1 -1
- suite_py/cli.py +107 -185
- suite_py/commands/aggregator.py +3 -5
- suite_py/commands/ask_review.py +3 -5
- suite_py/commands/batch_job.py +1 -2
- suite_py/commands/bump.py +1 -2
- suite_py/commands/check.py +3 -5
- suite_py/commands/context.py +26 -0
- suite_py/commands/create_branch.py +1 -2
- suite_py/commands/deploy.py +3 -5
- suite_py/commands/docker.py +1 -2
- suite_py/commands/generator.py +1 -2
- suite_py/commands/id.py +1 -2
- suite_py/commands/ip.py +1 -2
- suite_py/commands/login.py +4 -173
- suite_py/commands/merge_pr.py +3 -4
- suite_py/commands/open_pr.py +4 -5
- suite_py/commands/project_lock.py +3 -5
- suite_py/commands/release.py +3 -5
- suite_py/commands/secret.py +1 -2
- suite_py/commands/set_token.py +1 -2
- suite_py/commands/status.py +3 -4
- suite_py/lib/config.py +1 -3
- suite_py/lib/handler/captainhook_handler.py +44 -54
- suite_py/lib/handler/metrics_handler.py +8 -6
- suite_py/lib/handler/okta_handler.py +81 -0
- suite_py/lib/logger.py +1 -0
- suite_py/lib/metrics.py +4 -2
- suite_py/lib/oauth.py +156 -0
- suite_py/lib/tokens.py +4 -0
- {suite_py-1.41.2.dist-info → suite_py-1.41.4.dist-info}/METADATA +2 -4
- suite_py-1.41.4.dist-info/RECORD +54 -0
- suite_py/commands/qa.py +0 -424
- suite_py/lib/handler/qainit_handler.py +0 -259
- suite_py-1.41.2.dist-info/RECORD +0 -53
- /suite_py/{commands/templates → templates}/login.html +0 -0
- {suite_py-1.41.2.dist-info → suite_py-1.41.4.dist-info}/WHEEL +0 -0
- {suite_py-1.41.2.dist-info → suite_py-1.41.4.dist-info}/entry_points.txt +0 -0
suite_py/commands/qa.py
DELETED
|
@@ -1,424 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
|
|
3
|
-
import copy
|
|
4
|
-
import datetime
|
|
5
|
-
import json
|
|
6
|
-
import re
|
|
7
|
-
import sys
|
|
8
|
-
|
|
9
|
-
from dateutil import parser, tz
|
|
10
|
-
from rich.console import Console
|
|
11
|
-
from rich.table import Table
|
|
12
|
-
|
|
13
|
-
from suite_py.commands.login import Login
|
|
14
|
-
from suite_py.lib import logger
|
|
15
|
-
from suite_py.lib import metrics
|
|
16
|
-
from suite_py.lib.handler import git_handler as git
|
|
17
|
-
from suite_py.lib.handler import prompt_utils
|
|
18
|
-
from suite_py.lib.handler.drone_handler import DroneHandler
|
|
19
|
-
from suite_py.lib.handler.git_handler import GitHandler
|
|
20
|
-
from suite_py.lib.handler.qainit_handler import QainitHandler
|
|
21
|
-
from suite_py.lib.handler.youtrack_handler import YoutrackHandler
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class QA:
|
|
25
|
-
def __init__(self, action, project, config, tokens, flags=None):
|
|
26
|
-
self._action = action
|
|
27
|
-
self._project = project
|
|
28
|
-
self._flags = flags
|
|
29
|
-
self._config = config
|
|
30
|
-
self._tokens = tokens
|
|
31
|
-
self._git = GitHandler(project, config)
|
|
32
|
-
self._qainit = QainitHandler(project, config, tokens)
|
|
33
|
-
self._youtrack = YoutrackHandler(config, tokens)
|
|
34
|
-
self._drone = DroneHandler(config, tokens)
|
|
35
|
-
|
|
36
|
-
@metrics.command("qa")
|
|
37
|
-
def run(self):
|
|
38
|
-
if not self._qainit.user_info():
|
|
39
|
-
logger.warning("You're not logged in.")
|
|
40
|
-
Login(self._config).run()
|
|
41
|
-
|
|
42
|
-
if self._action == "list":
|
|
43
|
-
self._list()
|
|
44
|
-
elif self._action == "create":
|
|
45
|
-
self._create()
|
|
46
|
-
elif self._action == "update":
|
|
47
|
-
self._update()
|
|
48
|
-
elif self._action == "delete":
|
|
49
|
-
self._delete()
|
|
50
|
-
elif self._action == "freeze":
|
|
51
|
-
self._freeze()
|
|
52
|
-
elif self._action == "unfreeze":
|
|
53
|
-
self._unfreeze()
|
|
54
|
-
elif self._action == "check":
|
|
55
|
-
self._check()
|
|
56
|
-
elif self._action == "describe":
|
|
57
|
-
self._describe()
|
|
58
|
-
elif self._action == "update-quota":
|
|
59
|
-
self._update_quota()
|
|
60
|
-
elif self._action == "toggle-maintenance":
|
|
61
|
-
self._toggle_maintenance()
|
|
62
|
-
|
|
63
|
-
def _check(self):
|
|
64
|
-
logger.info(
|
|
65
|
-
"Checking configuration. If there is an issue, check ~/.suite_py/config.yml file and execute: suite-py login"
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
self._qainit.user_info()
|
|
69
|
-
|
|
70
|
-
def _clean_date(self, datetime_str):
|
|
71
|
-
# expected format: '2021-07-23T14:04:12.000000Z'
|
|
72
|
-
datetime_object = datetime.datetime.strptime(
|
|
73
|
-
datetime_str, "%Y-%m-%dT%H:%M:%S.%fZ"
|
|
74
|
-
)
|
|
75
|
-
# Define time zones:
|
|
76
|
-
utc_time_zone = tz.tzutc()
|
|
77
|
-
local_time_zone = tz.tzlocal()
|
|
78
|
-
# Convert time zone
|
|
79
|
-
utc_datetime_object = datetime_object.replace(tzinfo=utc_time_zone)
|
|
80
|
-
local_datetime_object = utc_datetime_object.astimezone(local_time_zone)
|
|
81
|
-
return local_datetime_object.strftime("%d/%m/%Y %H:%M:%S %z")
|
|
82
|
-
|
|
83
|
-
def _create_instance_table(self):
|
|
84
|
-
instance_table = Table()
|
|
85
|
-
instance_table.add_column("Name", style="purple")
|
|
86
|
-
instance_table.add_column("Hash", style="green", width=32)
|
|
87
|
-
instance_table.add_column("Card", style="white")
|
|
88
|
-
instance_table.add_column("Created by", style="white")
|
|
89
|
-
instance_table.add_column("Updated by", style="white")
|
|
90
|
-
instance_table.add_column("Deleted by", style="white")
|
|
91
|
-
instance_table.add_column("Last update", style="white")
|
|
92
|
-
instance_table.add_column("Status", style="white")
|
|
93
|
-
|
|
94
|
-
return instance_table
|
|
95
|
-
|
|
96
|
-
def _insert_instance_record(self, table, qa):
|
|
97
|
-
table.add_row(
|
|
98
|
-
qa["name"],
|
|
99
|
-
qa["hash"],
|
|
100
|
-
qa["card"],
|
|
101
|
-
(
|
|
102
|
-
qa.get("created", {}).get("github_username", "/")
|
|
103
|
-
if qa["created"] is not None
|
|
104
|
-
else "/"
|
|
105
|
-
),
|
|
106
|
-
(
|
|
107
|
-
qa.get("updated", {}).get("github_username", "/")
|
|
108
|
-
if qa["updated"] is not None
|
|
109
|
-
else "/"
|
|
110
|
-
),
|
|
111
|
-
(
|
|
112
|
-
qa.get("deleted", {}).get("github_username", "/")
|
|
113
|
-
if qa["deleted"] is not None
|
|
114
|
-
else "/"
|
|
115
|
-
),
|
|
116
|
-
self._clean_date(qa["updated_at"]),
|
|
117
|
-
qa["status"],
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
def _list(self):
|
|
121
|
-
# init empty table with column (useful for reset)
|
|
122
|
-
empty_table = self._create_instance_table()
|
|
123
|
-
table = copy.deepcopy(empty_table)
|
|
124
|
-
console = Console()
|
|
125
|
-
|
|
126
|
-
# execute query with pagination and filtering
|
|
127
|
-
page_number = 1
|
|
128
|
-
while True:
|
|
129
|
-
r = self._qainit.list(
|
|
130
|
-
self._flags,
|
|
131
|
-
page=page_number,
|
|
132
|
-
page_size=self._config.qainit["table_size"],
|
|
133
|
-
)
|
|
134
|
-
response = r.json()
|
|
135
|
-
qa_list = response["list"]
|
|
136
|
-
for qa in qa_list:
|
|
137
|
-
self._insert_instance_record(table, qa)
|
|
138
|
-
|
|
139
|
-
console.print(table)
|
|
140
|
-
|
|
141
|
-
# break conditions
|
|
142
|
-
if response["page_number"] >= response["total_pages"]:
|
|
143
|
-
break
|
|
144
|
-
if not prompt_utils.ask_confirm(
|
|
145
|
-
f"I found {response['total_entries']} results. Do you want to load a few more?",
|
|
146
|
-
False,
|
|
147
|
-
):
|
|
148
|
-
break
|
|
149
|
-
page_number += 1
|
|
150
|
-
# table reset
|
|
151
|
-
table = copy.deepcopy(empty_table)
|
|
152
|
-
|
|
153
|
-
def _describe(self):
|
|
154
|
-
qa_hash = self._flags["hash"]
|
|
155
|
-
jsonify = self._flags["json"]
|
|
156
|
-
|
|
157
|
-
r = self._qainit.describe(qa_hash)
|
|
158
|
-
|
|
159
|
-
issues = self._check_instance_issues(r)
|
|
160
|
-
if issues:
|
|
161
|
-
msg = "an issue" if len(issues) == 1 else f"{len(issues)} issues"
|
|
162
|
-
logger.warning(f"⚠️ suite-py diagnostic tool found {msg}:")
|
|
163
|
-
for i in issues:
|
|
164
|
-
logger.warning(i)
|
|
165
|
-
logger.warning(
|
|
166
|
-
"If the problem persist write a detailed message on #team-platform-operations"
|
|
167
|
-
)
|
|
168
|
-
sys.exit(-1)
|
|
169
|
-
|
|
170
|
-
if jsonify:
|
|
171
|
-
print(json.dumps(r, sort_keys=True, indent=2))
|
|
172
|
-
else:
|
|
173
|
-
# RESOURCES TABLE
|
|
174
|
-
table = Table()
|
|
175
|
-
table.add_column("Microservice", style="purple", no_wrap=True)
|
|
176
|
-
table.add_column("Drone build")
|
|
177
|
-
table.add_column("Branch", style="white")
|
|
178
|
-
table.add_column("Last update", style="white")
|
|
179
|
-
table.add_column("Status", style="white")
|
|
180
|
-
|
|
181
|
-
# INSTANCE TABLE
|
|
182
|
-
instance_table = self._create_instance_table()
|
|
183
|
-
|
|
184
|
-
self._insert_instance_record(instance_table, r["list"])
|
|
185
|
-
|
|
186
|
-
# DNS TABLE
|
|
187
|
-
dns_table = Table()
|
|
188
|
-
dns_table.add_column("Name", style="purple", no_wrap=True)
|
|
189
|
-
dns_table.add_column("Record", style="green")
|
|
190
|
-
|
|
191
|
-
console = Console()
|
|
192
|
-
|
|
193
|
-
try:
|
|
194
|
-
resources_list = sorted(r["list"]["resources"], key=lambda k: k["name"])
|
|
195
|
-
for resource in resources_list:
|
|
196
|
-
if (
|
|
197
|
-
(
|
|
198
|
-
resource["type"] == "microservice"
|
|
199
|
-
or "service" in resource["name"]
|
|
200
|
-
)
|
|
201
|
-
and "dns" in resource
|
|
202
|
-
and resource["dns"] is not None
|
|
203
|
-
):
|
|
204
|
-
for key, value in resource["dns"].items():
|
|
205
|
-
dns_table.add_row(key, value)
|
|
206
|
-
if resource["type"] == "microservice":
|
|
207
|
-
drone_url = (
|
|
208
|
-
(
|
|
209
|
-
"[blue][u]"
|
|
210
|
-
+ "https://drone-1.prima.it/primait/"
|
|
211
|
-
+ resource["name"]
|
|
212
|
-
+ "/"
|
|
213
|
-
+ resource["promoted_build"]
|
|
214
|
-
+ "[/u][/blue]"
|
|
215
|
-
)
|
|
216
|
-
if resource["promoted_build"]
|
|
217
|
-
else "Not available"
|
|
218
|
-
)
|
|
219
|
-
table.add_row(
|
|
220
|
-
resource["name"],
|
|
221
|
-
drone_url,
|
|
222
|
-
(
|
|
223
|
-
resource["ref"]
|
|
224
|
-
if resource["ref"] == "master"
|
|
225
|
-
else f"[green]{resource['ref']}[/green]"
|
|
226
|
-
),
|
|
227
|
-
self._clean_date(resource["updated_at"]),
|
|
228
|
-
resource["status"],
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
console.print(instance_table)
|
|
232
|
-
console.print(dns_table)
|
|
233
|
-
console.print(table)
|
|
234
|
-
except TypeError as e:
|
|
235
|
-
logger.error(f"Unexpected response format: {e}")
|
|
236
|
-
|
|
237
|
-
def _delete(self):
|
|
238
|
-
qa_hashes = self._flags["hashes"]
|
|
239
|
-
force = self._flags["force"]
|
|
240
|
-
for qa_hash in qa_hashes:
|
|
241
|
-
if force:
|
|
242
|
-
if not self._qainit.force_update(qa_hash):
|
|
243
|
-
# this function fails only if the QA is effectively stuck
|
|
244
|
-
# and qa init it's unable to solve it
|
|
245
|
-
logger.error(
|
|
246
|
-
"QA force deletion has failed, please contact #platform-operations team."
|
|
247
|
-
)
|
|
248
|
-
return
|
|
249
|
-
self._qainit.delete(qa_hash, force)
|
|
250
|
-
|
|
251
|
-
def _freeze(self):
|
|
252
|
-
self._qainit.freeze(self._flags["hash"])
|
|
253
|
-
|
|
254
|
-
def _unfreeze(self):
|
|
255
|
-
self._qainit.unfreeze(self._flags["hash"])
|
|
256
|
-
|
|
257
|
-
def _create(self):
|
|
258
|
-
user = self._qainit.user_info()
|
|
259
|
-
|
|
260
|
-
if not user["quota"]["remaining"] > 0:
|
|
261
|
-
logger.error("There's no remaining quota for you.")
|
|
262
|
-
sys.exit("-1")
|
|
263
|
-
|
|
264
|
-
if "staging" in self._qainit.url:
|
|
265
|
-
qa_default_name = (
|
|
266
|
-
f"staging_{git.get_username()}_{self._git.current_branch_name()}"
|
|
267
|
-
)
|
|
268
|
-
else:
|
|
269
|
-
qa_default_name = f"{git.get_username()}_{self._git.current_branch_name()}"
|
|
270
|
-
|
|
271
|
-
qa_name = prompt_utils.ask_questions_input(
|
|
272
|
-
"Choose the QA name: ", default_text=qa_default_name
|
|
273
|
-
)
|
|
274
|
-
|
|
275
|
-
card_match = re.match(r"[^\/]*_(?P<name>[A-Z]+-\d+)\/", qa_name)
|
|
276
|
-
default_card_name = (
|
|
277
|
-
card_match["name"] if card_match else self._config.user["default_slug"]
|
|
278
|
-
)
|
|
279
|
-
|
|
280
|
-
qa_card = prompt_utils.ask_questions_input(
|
|
281
|
-
"Youtrack issue ID: ", default_text=default_card_name
|
|
282
|
-
)
|
|
283
|
-
|
|
284
|
-
if qa_card != "":
|
|
285
|
-
try:
|
|
286
|
-
self._youtrack.get_issue(qa_card)
|
|
287
|
-
except Exception:
|
|
288
|
-
logger.error("invalid Youtrack issue ID")
|
|
289
|
-
sys.exit(-1)
|
|
290
|
-
|
|
291
|
-
self._qainit.create(qa_name, qa_card, self._flags["services"])
|
|
292
|
-
|
|
293
|
-
def _update(self):
|
|
294
|
-
self._qainit.update(self._flags["hash"], self._flags["services"])
|
|
295
|
-
|
|
296
|
-
def _update_quota(self):
|
|
297
|
-
username = prompt_utils.ask_questions_input("Insert GitHub username: ")
|
|
298
|
-
quota = prompt_utils.ask_questions_input("Insert new quota value: ")
|
|
299
|
-
|
|
300
|
-
self._qainit.update_user_quota(username, quota)
|
|
301
|
-
|
|
302
|
-
def _toggle_maintenance(self):
|
|
303
|
-
self._qainit.maintenance()
|
|
304
|
-
|
|
305
|
-
def _check_instance_issues(self, response):
|
|
306
|
-
issues = []
|
|
307
|
-
|
|
308
|
-
if not response["list"]:
|
|
309
|
-
issues.append("QA not found")
|
|
310
|
-
|
|
311
|
-
# Check if instance status is still pending
|
|
312
|
-
if response["list"] and response["list"]["status"] in ["creating", "updating"]:
|
|
313
|
-
microservices = self._filter_list(
|
|
314
|
-
"type", ["microservice"], response["list"]["resources"]
|
|
315
|
-
)
|
|
316
|
-
# 1. Microservices list is empty?
|
|
317
|
-
if len(microservices) == 0:
|
|
318
|
-
issues.append("Microservices list is empty, please be patient.")
|
|
319
|
-
return issues
|
|
320
|
-
|
|
321
|
-
# 2. Check if qainit worker is still launching updates
|
|
322
|
-
if not self._is_update_initiated(microservices, response["list"]):
|
|
323
|
-
issues.append(
|
|
324
|
-
"Qainit is still working on microservices, try again in a while."
|
|
325
|
-
)
|
|
326
|
-
issues.append("https://www.youtube.com/watch?v=dQw4w9WgXcQ")
|
|
327
|
-
return issues
|
|
328
|
-
|
|
329
|
-
# 3. Check every resource
|
|
330
|
-
stale_resources = self._filter_list(
|
|
331
|
-
"status", ["creating", "updating"], microservices
|
|
332
|
-
)
|
|
333
|
-
for resource in stale_resources:
|
|
334
|
-
issues += self._check_resource_issues(resource)
|
|
335
|
-
|
|
336
|
-
# 4. Check is instance is stuck in a stale status
|
|
337
|
-
if (
|
|
338
|
-
len(microservices) > 0
|
|
339
|
-
and len(stale_resources) == 0
|
|
340
|
-
and self._minutes_elapsed_since_update(response["list"]) >= 5
|
|
341
|
-
):
|
|
342
|
-
logger.warning("Trying to call qainit to force QA update...")
|
|
343
|
-
if self._qainit.force_update(response["list"]["hash"]):
|
|
344
|
-
issues.append(
|
|
345
|
-
"Your QA was in a stale status and has been fixed, launch `suite-py describe` again please."
|
|
346
|
-
)
|
|
347
|
-
else:
|
|
348
|
-
issues.append(
|
|
349
|
-
"Your QA was in a stale status but forced update has failed, please contact #platform-operations team."
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
return issues
|
|
353
|
-
|
|
354
|
-
def _is_update_initiated(self, microservices, instance):
|
|
355
|
-
microservices_updates = [
|
|
356
|
-
parser.parse(x["updated_at"]).timestamp() for x in microservices
|
|
357
|
-
]
|
|
358
|
-
max_microservice_update = max(microservices_updates)
|
|
359
|
-
instance_update = parser.parse(instance["updated_at"]).timestamp()
|
|
360
|
-
|
|
361
|
-
return max_microservice_update >= instance_update
|
|
362
|
-
|
|
363
|
-
def _minutes_elapsed_since_update(self, instance):
|
|
364
|
-
return (
|
|
365
|
-
datetime.datetime.now().timestamp()
|
|
366
|
-
- parser.parse(instance["updated_at"]).timestamp()
|
|
367
|
-
) / 60
|
|
368
|
-
|
|
369
|
-
def _check_resource_issues(self, resource):
|
|
370
|
-
issues = []
|
|
371
|
-
|
|
372
|
-
build = self._drone.get_repo_build(resource["name"], resource["promoted_build"])
|
|
373
|
-
|
|
374
|
-
if "stages" not in build:
|
|
375
|
-
issues.append("Suite-py is unable to locate drone build.")
|
|
376
|
-
return issues
|
|
377
|
-
|
|
378
|
-
qainit_step = self._filter_list("name", ["qainit-it"], build["stages"])[0]
|
|
379
|
-
# Check if build is succeded
|
|
380
|
-
if build["status"] == "success" or qainit_step["status"] == "error":
|
|
381
|
-
issues.append(
|
|
382
|
-
f"Something between Drone and Qainit went wrong, did you try to restart the build? {self._drone.get_repo_build_url(resource['name'], resource['promoted_build'])}"
|
|
383
|
-
)
|
|
384
|
-
|
|
385
|
-
# Check if build is killed
|
|
386
|
-
elif build["status"] == "killed":
|
|
387
|
-
issues.append(
|
|
388
|
-
f"Seems someone killed the build, did you try to restart the build? {self._drone.get_repo_build_url(resource['name'], resource['promoted_build'])}"
|
|
389
|
-
)
|
|
390
|
-
|
|
391
|
-
# Check if build is failed
|
|
392
|
-
elif build["status"] == "failure":
|
|
393
|
-
build_pipeline = self._filter_list("name", ["build_qa"], build["stages"])[0]
|
|
394
|
-
if build_pipeline and build_pipeline["status"] == "failed":
|
|
395
|
-
issues.append(
|
|
396
|
-
f"Something went wrong during microservice build step, check the logs {self._drone.get_build_and_pipeline_url(resource['name'], resource['promoted_build'], build_pipeline['number'])}"
|
|
397
|
-
)
|
|
398
|
-
|
|
399
|
-
deploy_pipeline = self._filter_list(
|
|
400
|
-
"name", ["deploy-it-qa"], build["stages"]
|
|
401
|
-
)[0]
|
|
402
|
-
if deploy_pipeline and deploy_pipeline["status"] == "failed":
|
|
403
|
-
issues.append(
|
|
404
|
-
f"Something went wront during microservice deploy step, check the logs {self._drone.get_build_and_pipeline_url(resource['name'], resource['promoted_build'], deploy_pipeline['number'])}"
|
|
405
|
-
)
|
|
406
|
-
# Check if build is running for over an hour
|
|
407
|
-
elif (
|
|
408
|
-
build["status"] == "running"
|
|
409
|
-
and self._difference_in_hours(build["started"]) >= 1
|
|
410
|
-
):
|
|
411
|
-
issues.append(
|
|
412
|
-
f"It looks like the build is stuck, did you try to restart the build? {self._drone.get_repo_build_url(resource['name'], resource['promoted_build'])}"
|
|
413
|
-
)
|
|
414
|
-
|
|
415
|
-
return issues
|
|
416
|
-
|
|
417
|
-
def _filter_list(self, search_key, in_list, search_list):
|
|
418
|
-
return list(filter(lambda l: l[search_key] in in_list, search_list))
|
|
419
|
-
|
|
420
|
-
def _difference_in_hours(self, unix_timestamp):
|
|
421
|
-
ts = datetime.datetime.fromtimestamp(unix_timestamp)
|
|
422
|
-
current_ts = datetime.datetime.utcnow()
|
|
423
|
-
|
|
424
|
-
return (current_ts - ts).total_seconds() / 60 / 60
|
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
|
2
|
-
import json
|
|
3
|
-
import sys
|
|
4
|
-
|
|
5
|
-
import requests
|
|
6
|
-
from github.GithubException import UnknownObjectException
|
|
7
|
-
from halo import Halo
|
|
8
|
-
|
|
9
|
-
from suite_py.lib import logger
|
|
10
|
-
from suite_py.lib.handler import prompt_utils
|
|
11
|
-
from suite_py.lib.handler.git_handler import GitHandler
|
|
12
|
-
from suite_py.lib.handler.github_handler import GithubHandler
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class QainitHandler:
|
|
16
|
-
|
|
17
|
-
scope_mapping = {
|
|
18
|
-
"admin": [
|
|
19
|
-
"update:user-quota",
|
|
20
|
-
"delete:others-qa",
|
|
21
|
-
"create:qa",
|
|
22
|
-
"delete:qa",
|
|
23
|
-
"describe:qa",
|
|
24
|
-
"describe:others-qa",
|
|
25
|
-
"update:qa",
|
|
26
|
-
"update:others-qa",
|
|
27
|
-
"list:qa",
|
|
28
|
-
"list:others-qa",
|
|
29
|
-
"access:captainhook",
|
|
30
|
-
],
|
|
31
|
-
"dev": [
|
|
32
|
-
"create:qa",
|
|
33
|
-
"delete:qa",
|
|
34
|
-
"describe:qa",
|
|
35
|
-
"describe:others-qa",
|
|
36
|
-
"update:qa",
|
|
37
|
-
"update:others-qa",
|
|
38
|
-
"list:qa",
|
|
39
|
-
"list:others-qa",
|
|
40
|
-
"access:captainhook",
|
|
41
|
-
],
|
|
42
|
-
"external": ["create:qa", "delete:qa", "describe:qa", "list:qa", "update:qa"],
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
def __init__(self, project, config, tokens):
|
|
46
|
-
self._project = project
|
|
47
|
-
self._token = tokens.drone
|
|
48
|
-
self._config = config
|
|
49
|
-
self.url = self._config.qainit["url"]
|
|
50
|
-
self.okta_tenant = self._config.okta["tenant"]
|
|
51
|
-
self.okta_token = self._config.get_cache(f"{self.okta_tenant}_okta_token")
|
|
52
|
-
self._github = GithubHandler(tokens)
|
|
53
|
-
self._git = GitHandler(project, config)
|
|
54
|
-
|
|
55
|
-
if "url" not in config.qainit:
|
|
56
|
-
self.usage()
|
|
57
|
-
sys.exit(-1)
|
|
58
|
-
|
|
59
|
-
def usage(self):
|
|
60
|
-
logger.warning(
|
|
61
|
-
"Unable to use QA commands: missing qainit config in ~/.suite_py/config.yml"
|
|
62
|
-
)
|
|
63
|
-
logger.warning(
|
|
64
|
-
"Update your config.yml as: https://github.com/primait/suite_py/blob/master/.config.yml.dist"
|
|
65
|
-
)
|
|
66
|
-
|
|
67
|
-
def user_info(self):
|
|
68
|
-
r = self._execute("GET", "/api/v1/user")
|
|
69
|
-
|
|
70
|
-
if r.status_code == 401:
|
|
71
|
-
return None
|
|
72
|
-
|
|
73
|
-
logger.debug(json.dumps(r.json(), indent=2))
|
|
74
|
-
|
|
75
|
-
return r.json()
|
|
76
|
-
|
|
77
|
-
def update_user_quota(self, username, quota):
|
|
78
|
-
body = {"github_username": f"{username}", "quota": f"{quota}"}
|
|
79
|
-
logger.debug(json.dumps(body))
|
|
80
|
-
r = self._execute(
|
|
81
|
-
"POST",
|
|
82
|
-
"/api/v1/user/quota",
|
|
83
|
-
body=json.dumps(body),
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
logger.info("Quota updated.")
|
|
87
|
-
logger.debug(json.dumps(r.json(), indent=2))
|
|
88
|
-
|
|
89
|
-
def create(self, name, card, services):
|
|
90
|
-
srv_list = self.create_services_body(services)
|
|
91
|
-
body = {"name": name, "card": card, "services": srv_list}
|
|
92
|
-
|
|
93
|
-
logger.debug(json.dumps(body))
|
|
94
|
-
r = self._execute(
|
|
95
|
-
"POST",
|
|
96
|
-
"/api/v1/qa",
|
|
97
|
-
body=json.dumps(body),
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
logger.info(f"QA creation initiated. Your namespace hash: {r.json()['hash']}")
|
|
101
|
-
logger.debug(json.dumps(r.json(), indent=2))
|
|
102
|
-
|
|
103
|
-
def update(self, qa_hash, services):
|
|
104
|
-
srv_list = self.create_services_body(services)
|
|
105
|
-
|
|
106
|
-
body = {"services": srv_list}
|
|
107
|
-
logger.debug(json.dumps(body))
|
|
108
|
-
|
|
109
|
-
r = self._execute(
|
|
110
|
-
"PUT",
|
|
111
|
-
f"/api/v1/qa/{qa_hash}",
|
|
112
|
-
body=json.dumps(body),
|
|
113
|
-
)
|
|
114
|
-
self.handle_call_result(r, "update", qa_hash)
|
|
115
|
-
|
|
116
|
-
def list(self, params, page=1, page_size=10):
|
|
117
|
-
filters = []
|
|
118
|
-
status_values = '"created","creating","updated","updating","failed","frozen","freezing","unfreezing"'
|
|
119
|
-
if len(params["status"]) > 0:
|
|
120
|
-
status_values = ",".join([f'"{status}"' for status in params["status"]])
|
|
121
|
-
filters.append(f"status=[{status_values}]")
|
|
122
|
-
|
|
123
|
-
if params["user"] is not None:
|
|
124
|
-
filters.append(f"user={params['user']}")
|
|
125
|
-
|
|
126
|
-
if params["card"] is not None:
|
|
127
|
-
filters.append(f"card={params['card']}")
|
|
128
|
-
|
|
129
|
-
filters_string = "&".join(filters)
|
|
130
|
-
|
|
131
|
-
return self._execute(
|
|
132
|
-
"GET",
|
|
133
|
-
f"/api/v1/qa?{filters_string}&page_size={page_size}&page={page}",
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
def delete(self, qa_hash, force=False):
|
|
137
|
-
r = self._execute(
|
|
138
|
-
"DELETE",
|
|
139
|
-
f"/api/v1/qa/{qa_hash}",
|
|
140
|
-
)
|
|
141
|
-
logger.debug(json.dumps(r.json(), indent=2))
|
|
142
|
-
if r.ok:
|
|
143
|
-
logger.info(f"QA {qa_hash} deletion initiated")
|
|
144
|
-
else:
|
|
145
|
-
logger.error(
|
|
146
|
-
f"QA {qa_hash} deletion has failed{', try running this command with --force flag' if not force else ''}."
|
|
147
|
-
)
|
|
148
|
-
|
|
149
|
-
def describe(self, qa_hash):
|
|
150
|
-
r = self._execute(
|
|
151
|
-
"GET",
|
|
152
|
-
f"/api/v1/qa/{qa_hash}",
|
|
153
|
-
).json()
|
|
154
|
-
logger.debug(json.dumps(r, indent=2))
|
|
155
|
-
|
|
156
|
-
return r
|
|
157
|
-
|
|
158
|
-
def create_services_body(self, prj_list):
|
|
159
|
-
srv_list = []
|
|
160
|
-
ref = self._git.current_branch_name()
|
|
161
|
-
for prj in prj_list:
|
|
162
|
-
with Halo(text="Loading branches...", spinner="dots", color="magenta"):
|
|
163
|
-
choices = [
|
|
164
|
-
{"name": branch.name, "value": branch.name}
|
|
165
|
-
for branch in self._github.get_branches(prj)
|
|
166
|
-
]
|
|
167
|
-
if choices:
|
|
168
|
-
choices.sort(key=lambda x: x["name"])
|
|
169
|
-
ref = prompt_utils.ask_choices(
|
|
170
|
-
f"Select branch for project - {prj}: ", choices, default_text=ref
|
|
171
|
-
)
|
|
172
|
-
try:
|
|
173
|
-
self._github.get_raw_content(prj, ref, ".service.yml")
|
|
174
|
-
except UnknownObjectException:
|
|
175
|
-
logger.error(
|
|
176
|
-
f".service.yml missing for project: {prj}, can't add microservice to QA"
|
|
177
|
-
)
|
|
178
|
-
sys.exit(-1)
|
|
179
|
-
srv_list.append(
|
|
180
|
-
{
|
|
181
|
-
"name": prj,
|
|
182
|
-
"ref": ref,
|
|
183
|
-
}
|
|
184
|
-
)
|
|
185
|
-
|
|
186
|
-
return srv_list
|
|
187
|
-
|
|
188
|
-
def freeze(self, qa_hash):
|
|
189
|
-
body = {"operation": "freeze"}
|
|
190
|
-
logger.debug(json.dumps(body))
|
|
191
|
-
r = self._execute("PUT", f"/api/v1/qa/{qa_hash}", body=json.dumps(body))
|
|
192
|
-
self.handle_call_result(r, "freezing", qa_hash)
|
|
193
|
-
|
|
194
|
-
def unfreeze(self, qa_hash):
|
|
195
|
-
body = {"operation": "unfreeze"}
|
|
196
|
-
logger.debug(json.dumps(body))
|
|
197
|
-
r = self._execute("PUT", f"/api/v1/qa/{qa_hash}", body=json.dumps(body))
|
|
198
|
-
self.handle_call_result(r, "unfreezing", qa_hash)
|
|
199
|
-
|
|
200
|
-
def maintenance(self):
|
|
201
|
-
r = self._execute("POST", "/api/v1/maintenance")
|
|
202
|
-
if r.json()["success"]:
|
|
203
|
-
maintenance = r.json()["maintenance"]
|
|
204
|
-
logger.info(
|
|
205
|
-
f"Maintenance mode is now {'enabled' if maintenance else 'disabled'}"
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
logger.debug(json.dumps(r.json(), indent=2))
|
|
209
|
-
|
|
210
|
-
def force_update(self, qa_hash):
|
|
211
|
-
r = self._execute("POST", f"/api/v1/qa/shit-shoveler/force-update/{qa_hash}")
|
|
212
|
-
if r.status_code == 400:
|
|
213
|
-
msg = r.json()["message"]
|
|
214
|
-
if msg == "instance_not_force_updated":
|
|
215
|
-
logger.error("Failed to force update QA.")
|
|
216
|
-
return False
|
|
217
|
-
if msg == "not_in_stale_state":
|
|
218
|
-
logger.warning("QA is not in a stale state.")
|
|
219
|
-
return True
|
|
220
|
-
raise ValueError(
|
|
221
|
-
"Unexpected return type from qa init shit-shoveler endpoint."
|
|
222
|
-
)
|
|
223
|
-
if r.status_code == 500:
|
|
224
|
-
logger.error("Something went wrong")
|
|
225
|
-
return False
|
|
226
|
-
logger.info("QA has been forced to update")
|
|
227
|
-
logger.debug(json.dumps(r.json(), indent=2))
|
|
228
|
-
return True
|
|
229
|
-
|
|
230
|
-
def _execute(self, request_method, api_endpoint, body=None):
|
|
231
|
-
api_url = self.url + api_endpoint
|
|
232
|
-
okta_token = self._config.get_cache(f"{self.okta_tenant}_okta_token")
|
|
233
|
-
|
|
234
|
-
headers = {
|
|
235
|
-
"Content-Type": "application/json",
|
|
236
|
-
"Authorization": f"Bearer {okta_token}",
|
|
237
|
-
}
|
|
238
|
-
logger.debug(request_method)
|
|
239
|
-
logger.debug(api_url)
|
|
240
|
-
logger.debug(headers)
|
|
241
|
-
logger.debug(body)
|
|
242
|
-
# pylint: disable-next=missing-timeout
|
|
243
|
-
r = requests.request(request_method, api_url, headers=headers, data=body)
|
|
244
|
-
|
|
245
|
-
if r.ok:
|
|
246
|
-
logger.debug("Call to qainit-evo executed successfully")
|
|
247
|
-
else:
|
|
248
|
-
logger.error("Some issue during call to qainit-evo: ")
|
|
249
|
-
logger.error(f"Status code: {r.status_code}, response: {r.text}")
|
|
250
|
-
logger.debug(api_endpoint)
|
|
251
|
-
|
|
252
|
-
return r
|
|
253
|
-
|
|
254
|
-
def handle_call_result(self, req, operation, qa_hash):
|
|
255
|
-
if req.ok:
|
|
256
|
-
logger.info(f"QA {operation} initiated")
|
|
257
|
-
else:
|
|
258
|
-
logger.debug(json.dumps(req.json(), indent=2))
|
|
259
|
-
logger.info(f"Run suite-py qa describe {qa_hash} to check the status")
|