locust-cloud 1.12.4__py3-none-any.whl → 1.13.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.
- locust_cloud/cloud.py +244 -106
- {locust_cloud-1.12.4.dist-info → locust_cloud-1.13.0.dist-info}/METADATA +3 -4
- locust_cloud-1.13.0.dist-info/RECORD +5 -0
- locust_cloud/__init__.py +0 -133
- locust_cloud/auth.py +0 -443
- locust_cloud/credential_manager.py +0 -141
- locust_cloud/idle_exit.py +0 -38
- locust_cloud/socket_logging.py +0 -127
- locust_cloud/timescale/exporter.py +0 -313
- locust_cloud/timescale/queries.py +0 -321
- locust_cloud/timescale/query.py +0 -74
- locust_cloud/webui/.gitignore +0 -4
- locust_cloud/webui/.prettierrc +0 -9
- locust_cloud/webui/dist/assets/index-D3YieuNV.js +0 -329
- locust_cloud/webui/dist/index.html +0 -20
- locust_cloud/webui/eslint.config.mjs +0 -83
- locust_cloud/webui/index.html +0 -20
- locust_cloud/webui/package.json +0 -52
- locust_cloud/webui/tsconfig.json +0 -27
- locust_cloud/webui/tsconfig.tsbuildinfo +0 -1
- locust_cloud/webui/vite.config.ts +0 -9
- locust_cloud/webui/vitest.config.ts +0 -16
- locust_cloud/webui/yarn.lock +0 -5816
- locust_cloud-1.12.4.dist-info/RECORD +0 -25
- {locust_cloud-1.12.4.dist-info → locust_cloud-1.13.0.dist-info}/WHEEL +0 -0
- {locust_cloud-1.12.4.dist-info → locust_cloud-1.13.0.dist-info}/entry_points.txt +0 -0
locust_cloud/auth.py
DELETED
@@ -1,443 +0,0 @@
|
|
1
|
-
import logging
|
2
|
-
import os
|
3
|
-
from datetime import UTC, datetime, timedelta
|
4
|
-
from typing import Any, TypedDict, cast
|
5
|
-
|
6
|
-
import locust.env
|
7
|
-
import requests
|
8
|
-
import werkzeug
|
9
|
-
from flask import Blueprint, redirect, request, session, url_for
|
10
|
-
from flask_login import UserMixin, login_required, login_user, logout_user
|
11
|
-
from locust.html import render_template_from
|
12
|
-
from locust_cloud import __version__
|
13
|
-
|
14
|
-
logger = logging.getLogger(__name__)
|
15
|
-
|
16
|
-
|
17
|
-
class Credentials(TypedDict):
|
18
|
-
user_sub_id: str
|
19
|
-
refresh_token: str
|
20
|
-
|
21
|
-
|
22
|
-
class AuthUser(UserMixin):
|
23
|
-
def __init__(self, user_sub_id: str):
|
24
|
-
self.user_sub_id = user_sub_id
|
25
|
-
|
26
|
-
def get_id(self):
|
27
|
-
return self.user_sub_id
|
28
|
-
|
29
|
-
|
30
|
-
def set_credentials(username: str, credentials: Credentials, response: werkzeug.wrappers.response.Response):
|
31
|
-
if not credentials.get("user_sub_id"):
|
32
|
-
return response
|
33
|
-
|
34
|
-
user_sub_id = credentials["user_sub_id"]
|
35
|
-
refresh_token = credentials["refresh_token"]
|
36
|
-
|
37
|
-
response.set_cookie("username", username, expires=datetime.now(tz=UTC) + timedelta(days=365))
|
38
|
-
response.set_cookie("user_token", refresh_token, expires=datetime.now(tz=UTC) + timedelta(days=365))
|
39
|
-
response.set_cookie("user_sub_id", user_sub_id, expires=datetime.now(tz=UTC) + timedelta(days=365))
|
40
|
-
|
41
|
-
return response
|
42
|
-
|
43
|
-
|
44
|
-
def register_auth(environment: locust.env.Environment):
|
45
|
-
environment.web_ui.app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "") + os.getenv("CUSTOMER_ID", "")
|
46
|
-
environment.web_ui.app.debug = False
|
47
|
-
|
48
|
-
web_base_path = environment.parsed_options.web_base_path
|
49
|
-
auth_blueprint = Blueprint("locust_cloud_auth", __name__, url_prefix=web_base_path)
|
50
|
-
|
51
|
-
def load_user(user_sub_id: str):
|
52
|
-
username = request.cookies.get("username")
|
53
|
-
refresh_token = request.cookies.get("user_token")
|
54
|
-
|
55
|
-
if refresh_token:
|
56
|
-
environment.web_ui.template_args["username"] = username
|
57
|
-
return AuthUser(user_sub_id)
|
58
|
-
|
59
|
-
return None
|
60
|
-
|
61
|
-
environment.web_ui.login_manager.user_loader(load_user)
|
62
|
-
environment.web_ui.auth_args = cast(
|
63
|
-
Any,
|
64
|
-
{
|
65
|
-
"username_password_callback": f"{web_base_path}/authenticate",
|
66
|
-
},
|
67
|
-
)
|
68
|
-
|
69
|
-
environment.web_ui.auth_args["auth_providers"] = []
|
70
|
-
if environment.parsed_options.allow_signup:
|
71
|
-
environment.web_ui.auth_args["auth_providers"].append(
|
72
|
-
{"label": "Sign Up", "callback_url": f"{web_base_path}/signup"}
|
73
|
-
)
|
74
|
-
if environment.parsed_options.allow_forgot_password:
|
75
|
-
environment.web_ui.auth_args["auth_providers"].append(
|
76
|
-
{"label": "Forgot Password?", "callback_url": f"{web_base_path}/forgot-password"}
|
77
|
-
)
|
78
|
-
|
79
|
-
@auth_blueprint.route("/authenticate", methods=["POST"])
|
80
|
-
def login_submit():
|
81
|
-
username = request.form.get("username", "")
|
82
|
-
password = request.form.get("password")
|
83
|
-
|
84
|
-
try:
|
85
|
-
auth_response = requests.post(
|
86
|
-
f"{environment.parsed_options.deployer_url}/auth/login",
|
87
|
-
json={"username": username, "password": password},
|
88
|
-
headers={"X-Client-Version": __version__},
|
89
|
-
)
|
90
|
-
|
91
|
-
auth_response.raise_for_status()
|
92
|
-
|
93
|
-
credentials = auth_response.json()
|
94
|
-
|
95
|
-
if credentials.get("challenge_session"):
|
96
|
-
session["challenge_session"] = credentials.get("challenge_session")
|
97
|
-
session["username"] = username
|
98
|
-
|
99
|
-
session["auth_error"] = ""
|
100
|
-
|
101
|
-
return redirect(url_for("locust_cloud_auth.password_reset"))
|
102
|
-
if os.getenv("CUSTOMER_ID", "") and credentials.get("customer_id") != os.getenv("CUSTOMER_ID", ""):
|
103
|
-
session["auth_error"] = "Invalid login for this deployment"
|
104
|
-
return redirect(url_for("locust.login"))
|
105
|
-
|
106
|
-
if not credentials.get("user_sub_id"):
|
107
|
-
session["auth_error"] = "Unknown error during authentication, check logs and/or contact support"
|
108
|
-
return redirect(url_for("locust.login"))
|
109
|
-
|
110
|
-
response = redirect(url_for("locust.index"))
|
111
|
-
response = set_credentials(username, credentials, response)
|
112
|
-
login_user(AuthUser(credentials["user_sub_id"]))
|
113
|
-
|
114
|
-
return response
|
115
|
-
except requests.exceptions.HTTPError as e:
|
116
|
-
if e.response.status_code == 401:
|
117
|
-
session["auth_error"] = "Invalid username or password"
|
118
|
-
else:
|
119
|
-
logger.error(f"Unknown response from auth: {e.response.status_code} {e.response.text}")
|
120
|
-
|
121
|
-
session["auth_error"] = "Unknown error during authentication, check logs and/or contact support"
|
122
|
-
|
123
|
-
return redirect(url_for("locust.login"))
|
124
|
-
|
125
|
-
@auth_blueprint.route("/signup")
|
126
|
-
def signup():
|
127
|
-
if not environment.parsed_options.allow_signup:
|
128
|
-
return redirect(url_for("locust.login"))
|
129
|
-
|
130
|
-
if session.get("username"):
|
131
|
-
sign_up_args = {
|
132
|
-
"custom_form": {
|
133
|
-
"inputs": [
|
134
|
-
{
|
135
|
-
"label": "Confirmation Code",
|
136
|
-
"name": "confirmation_code",
|
137
|
-
"is_required": True,
|
138
|
-
},
|
139
|
-
],
|
140
|
-
"callback_url": f"{web_base_path}/confirm-signup",
|
141
|
-
"submit_button_text": "Confirm Email",
|
142
|
-
},
|
143
|
-
}
|
144
|
-
else:
|
145
|
-
sign_up_args = {
|
146
|
-
"custom_form": {
|
147
|
-
"inputs": [
|
148
|
-
{
|
149
|
-
"label": "Username",
|
150
|
-
"name": "username",
|
151
|
-
"is_required": True,
|
152
|
-
"type": "email",
|
153
|
-
},
|
154
|
-
{
|
155
|
-
"label": "Full Name",
|
156
|
-
"name": "customer_name",
|
157
|
-
"is_required": True,
|
158
|
-
},
|
159
|
-
{
|
160
|
-
"label": "Password",
|
161
|
-
"name": "password",
|
162
|
-
"is_secret": True,
|
163
|
-
"is_required": True,
|
164
|
-
},
|
165
|
-
{
|
166
|
-
"label": "Access Code",
|
167
|
-
"name": "access_code",
|
168
|
-
"is_required": True,
|
169
|
-
},
|
170
|
-
{
|
171
|
-
"label": "I consent to:\n\n1. Only test your own website/service or our example target\n\n2. Only use locust-cloud for its intended purpose: to load test other sites/services.\n\n3. Not attempt to circumvent your account limitations (e.g. max user count or max request count)\n\n4. Not use personal data (real names, addresses etc) in your tests.",
|
172
|
-
"name": "consent",
|
173
|
-
"default_value": False,
|
174
|
-
"is_required": True,
|
175
|
-
},
|
176
|
-
],
|
177
|
-
"callback_url": f"{web_base_path}/create-account",
|
178
|
-
"submit_button_text": "Sign Up",
|
179
|
-
},
|
180
|
-
}
|
181
|
-
|
182
|
-
if session.get("auth_info"):
|
183
|
-
sign_up_args["info"] = session["auth_info"]
|
184
|
-
if session.get("auth_sign_up_error"):
|
185
|
-
sign_up_args["error"] = session["auth_sign_up_error"]
|
186
|
-
|
187
|
-
return render_template_from(
|
188
|
-
"auth.html",
|
189
|
-
auth_args=sign_up_args,
|
190
|
-
)
|
191
|
-
|
192
|
-
@auth_blueprint.route("/create-account", methods=["POST"])
|
193
|
-
def create_account():
|
194
|
-
if not environment.parsed_options.allow_signup:
|
195
|
-
return redirect(url_for("locust.login"))
|
196
|
-
|
197
|
-
session["auth_sign_up_error"] = ""
|
198
|
-
session["auth_info"] = ""
|
199
|
-
|
200
|
-
username = request.form.get("username", "")
|
201
|
-
customer_name = request.form.get("customer_name", "")
|
202
|
-
password = request.form.get("password")
|
203
|
-
access_code = request.form.get("access_code")
|
204
|
-
|
205
|
-
try:
|
206
|
-
auth_response = requests.post(
|
207
|
-
f"{environment.parsed_options.deployer_url}/auth/signup",
|
208
|
-
json={"username": username, "password": password, "access_code": access_code},
|
209
|
-
)
|
210
|
-
|
211
|
-
auth_response.raise_for_status()
|
212
|
-
|
213
|
-
session["user_sub_id"] = auth_response.json().get("user_sub_id")
|
214
|
-
session["username"] = username
|
215
|
-
session["customer_name"] = customer_name
|
216
|
-
session["auth_info"] = (
|
217
|
-
"Please check your email and enter the confirmation code. If you didn't get a code after one minute, you can [request a new one](/resend-code)"
|
218
|
-
)
|
219
|
-
|
220
|
-
return redirect(url_for("locust_cloud_auth.signup"))
|
221
|
-
except requests.exceptions.HTTPError as e:
|
222
|
-
message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
|
223
|
-
session["auth_info"] = ""
|
224
|
-
session["auth_sign_up_error"] = message
|
225
|
-
|
226
|
-
return redirect(url_for("locust_cloud_auth.signup"))
|
227
|
-
|
228
|
-
@auth_blueprint.route("/resend-code")
|
229
|
-
def resend_code():
|
230
|
-
if not session.get("username"):
|
231
|
-
session["auth_sign_up_error"] = "An unexpected error occured. Please try again."
|
232
|
-
return redirect(url_for("locust_cloud_auth.signup"))
|
233
|
-
|
234
|
-
try:
|
235
|
-
auth_response = requests.post(
|
236
|
-
f"{environment.parsed_options.deployer_url}/auth/resend-confirmation",
|
237
|
-
json={"username": session.get("username")},
|
238
|
-
)
|
239
|
-
|
240
|
-
auth_response.raise_for_status()
|
241
|
-
|
242
|
-
session["auth_sign_up_error"] = ""
|
243
|
-
session["auth_info"] = "Confirmation code sent, please check your email."
|
244
|
-
|
245
|
-
return redirect(url_for("locust_cloud_auth.signup"))
|
246
|
-
except requests.exceptions.HTTPError as e:
|
247
|
-
message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
|
248
|
-
session["auth_info"] = ""
|
249
|
-
session["auth_sign_up_error"] = message
|
250
|
-
|
251
|
-
return redirect(url_for("locust_cloud_auth.signup"))
|
252
|
-
|
253
|
-
@auth_blueprint.route("/confirm-signup", methods=["POST"])
|
254
|
-
def confirm_signup():
|
255
|
-
if not environment.parsed_options.allow_signup:
|
256
|
-
return redirect(url_for("locust.login"))
|
257
|
-
if not session.get("user_sub_id"):
|
258
|
-
session["auth_sign_up_error"] = "An unexpected error occured. Please try again."
|
259
|
-
return redirect(url_for("locust_cloud_auth.signup"))
|
260
|
-
|
261
|
-
session["auth_sign_up_error"] = ""
|
262
|
-
confirmation_code = request.form.get("confirmation_code")
|
263
|
-
|
264
|
-
try:
|
265
|
-
auth_response = requests.post(
|
266
|
-
f"{environment.parsed_options.deployer_url}/auth/confirm-signup",
|
267
|
-
json={
|
268
|
-
"username": session.get("username"),
|
269
|
-
"customer_name": session.get("customer_name"),
|
270
|
-
"user_sub_id": session["user_sub_id"],
|
271
|
-
"confirmation_code": confirmation_code,
|
272
|
-
},
|
273
|
-
)
|
274
|
-
|
275
|
-
auth_response.raise_for_status()
|
276
|
-
|
277
|
-
session["username"] = None
|
278
|
-
session["auth_info"] = "Account created successfully!"
|
279
|
-
session["auth_sign_up_error"] = ""
|
280
|
-
|
281
|
-
return redirect("https://docs.locust.cloud/")
|
282
|
-
except requests.exceptions.HTTPError as e:
|
283
|
-
message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
|
284
|
-
session["auth_info"] = ""
|
285
|
-
session["auth_sign_up_error"] = message
|
286
|
-
|
287
|
-
return redirect(url_for("locust_cloud_auth.signup"))
|
288
|
-
|
289
|
-
@auth_blueprint.route("/forgot-password")
|
290
|
-
def forgot_password():
|
291
|
-
if not environment.parsed_options.allow_forgot_password:
|
292
|
-
return redirect(url_for("locust.login"))
|
293
|
-
|
294
|
-
forgot_password_args = {
|
295
|
-
"custom_form": {
|
296
|
-
"inputs": [
|
297
|
-
{
|
298
|
-
"label": "Username",
|
299
|
-
"name": "username",
|
300
|
-
"is_required": True,
|
301
|
-
"type": "email",
|
302
|
-
},
|
303
|
-
],
|
304
|
-
"callback_url": f"{web_base_path}/send-forgot-password",
|
305
|
-
"submit_button_text": "Reset Password",
|
306
|
-
},
|
307
|
-
"info": "Enter your email and we will send a code to reset your password",
|
308
|
-
}
|
309
|
-
|
310
|
-
if session.get("auth_error"):
|
311
|
-
forgot_password_args["error"] = session["auth_error"]
|
312
|
-
|
313
|
-
return render_template_from("auth.html", auth_args=forgot_password_args)
|
314
|
-
|
315
|
-
@auth_blueprint.route("/send-forgot-password", methods=["POST"])
|
316
|
-
def send_forgot_password():
|
317
|
-
if not environment.parsed_options.allow_forgot_password:
|
318
|
-
return redirect(url_for("locust.login"))
|
319
|
-
|
320
|
-
try:
|
321
|
-
username = request.form.get("username", "")
|
322
|
-
|
323
|
-
auth_response = requests.post(
|
324
|
-
f"{environment.parsed_options.deployer_url}/auth/forgot-password",
|
325
|
-
json={"username": username},
|
326
|
-
)
|
327
|
-
|
328
|
-
auth_response.raise_for_status()
|
329
|
-
|
330
|
-
session["username"] = username
|
331
|
-
|
332
|
-
return redirect(url_for("locust_cloud_auth.password_reset"))
|
333
|
-
except requests.exceptions.HTTPError as e:
|
334
|
-
message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
|
335
|
-
session["auth_info"] = ""
|
336
|
-
session["auth_error"] = message
|
337
|
-
|
338
|
-
return redirect(url_for("locust_cloud_auth.forgot_password"))
|
339
|
-
|
340
|
-
@auth_blueprint.route("/password-reset")
|
341
|
-
def password_reset():
|
342
|
-
if not environment.parsed_options.allow_forgot_password and not session.get("challenge_session"):
|
343
|
-
return redirect(url_for("locust.login"))
|
344
|
-
|
345
|
-
if session.get("challenge_session"):
|
346
|
-
reset_password_args = {
|
347
|
-
"custom_form": {
|
348
|
-
"inputs": [
|
349
|
-
{
|
350
|
-
"label": "New Password",
|
351
|
-
"name": "new_password",
|
352
|
-
"is_required": True,
|
353
|
-
"is_secret": True,
|
354
|
-
},
|
355
|
-
],
|
356
|
-
"callback_url": f"{web_base_path}/confirm-reset-password",
|
357
|
-
"submit_button_text": "Set Password",
|
358
|
-
},
|
359
|
-
"info": "You must set a new password",
|
360
|
-
}
|
361
|
-
else:
|
362
|
-
reset_password_args = {
|
363
|
-
"custom_form": {
|
364
|
-
"inputs": [
|
365
|
-
{
|
366
|
-
"label": "Confirmation Code",
|
367
|
-
"name": "confirmation_code",
|
368
|
-
"is_required": True,
|
369
|
-
},
|
370
|
-
{
|
371
|
-
"label": "New Password",
|
372
|
-
"name": "new_password",
|
373
|
-
"is_required": True,
|
374
|
-
"is_secret": True,
|
375
|
-
},
|
376
|
-
],
|
377
|
-
"callback_url": f"{web_base_path}/confirm-reset-password",
|
378
|
-
"submit_button_text": "Reset Password",
|
379
|
-
},
|
380
|
-
"info": "Enter your the confirmation code that was sent to your email",
|
381
|
-
}
|
382
|
-
|
383
|
-
if session.get("auth_error"):
|
384
|
-
reset_password_args["error"] = session["auth_error"]
|
385
|
-
|
386
|
-
return render_template_from("auth.html", auth_args=reset_password_args)
|
387
|
-
|
388
|
-
@auth_blueprint.route("/confirm-reset-password", methods=["POST"])
|
389
|
-
def confirm_reset_password():
|
390
|
-
if not environment.parsed_options.allow_forgot_password and not session.get("challenge_session"):
|
391
|
-
return redirect(url_for("locust.login"))
|
392
|
-
|
393
|
-
try:
|
394
|
-
username = session["username"]
|
395
|
-
confirmation_code = request.form.get("confirmation_code")
|
396
|
-
new_password = request.form.get("new_password")
|
397
|
-
|
398
|
-
auth_response = requests.post(
|
399
|
-
f"{environment.parsed_options.deployer_url}/auth/password-reset",
|
400
|
-
json={
|
401
|
-
"username": username,
|
402
|
-
"confirmation_code": confirmation_code,
|
403
|
-
"new_password": new_password,
|
404
|
-
"challenge_session": session.get("challenge_session"),
|
405
|
-
},
|
406
|
-
)
|
407
|
-
|
408
|
-
auth_response.raise_for_status()
|
409
|
-
|
410
|
-
session["username"] = ""
|
411
|
-
session["auth_error"] = ""
|
412
|
-
|
413
|
-
if session.get("challenge_session"):
|
414
|
-
session["challenge_session"] = ""
|
415
|
-
|
416
|
-
return redirect(url_for("locust_cloud_auth.password_reset_success"))
|
417
|
-
|
418
|
-
session["auth_info"] = "Password reset successfully! Please login"
|
419
|
-
|
420
|
-
return redirect(url_for("locust.login"))
|
421
|
-
except requests.exceptions.HTTPError as e:
|
422
|
-
message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
|
423
|
-
session["auth_info"] = ""
|
424
|
-
session["auth_error"] = message
|
425
|
-
|
426
|
-
return redirect(url_for("locust_cloud_auth.password_reset"))
|
427
|
-
|
428
|
-
@auth_blueprint.route("/password-reset-success")
|
429
|
-
def password_reset_success():
|
430
|
-
return render_template_from(
|
431
|
-
"auth.html",
|
432
|
-
auth_args={
|
433
|
-
"info": "Password successfully set! Please review the [documentation](https://docs.locust.cloud/) and start your first testrun! If you have already ran some tests, you may also [login](/login)"
|
434
|
-
},
|
435
|
-
)
|
436
|
-
|
437
|
-
@auth_blueprint.route("/logout", methods=["POST"])
|
438
|
-
@login_required
|
439
|
-
def logout():
|
440
|
-
logout_user()
|
441
|
-
return redirect(url_for("locust.login"))
|
442
|
-
|
443
|
-
environment.web_ui.app.register_blueprint(auth_blueprint)
|
@@ -1,141 +0,0 @@
|
|
1
|
-
import logging
|
2
|
-
import time
|
3
|
-
from datetime import UTC, datetime
|
4
|
-
from typing import Any
|
5
|
-
|
6
|
-
import boto3
|
7
|
-
import jwt
|
8
|
-
import requests
|
9
|
-
from botocore.credentials import RefreshableCredentials
|
10
|
-
from botocore.session import Session as BotocoreSession
|
11
|
-
from locust_cloud import __version__
|
12
|
-
|
13
|
-
logger = logging.getLogger(__name__)
|
14
|
-
|
15
|
-
|
16
|
-
class CredentialError(Exception):
|
17
|
-
"""Custom exception for credential-related errors."""
|
18
|
-
|
19
|
-
pass
|
20
|
-
|
21
|
-
|
22
|
-
class CredentialManager:
|
23
|
-
def __init__(
|
24
|
-
self,
|
25
|
-
lambda_url: str,
|
26
|
-
username: str | None = None,
|
27
|
-
password: str | None = None,
|
28
|
-
user_sub_id: str | None = None,
|
29
|
-
refresh_token: str | None = None,
|
30
|
-
access_key: str | None = None,
|
31
|
-
secret_key: str | None = None,
|
32
|
-
) -> None:
|
33
|
-
self.lambda_url = lambda_url
|
34
|
-
self.username = username
|
35
|
-
self.password = password
|
36
|
-
self.user_sub_id = user_sub_id
|
37
|
-
self.refresh_token = refresh_token
|
38
|
-
|
39
|
-
self.credentials = {
|
40
|
-
"access_key": access_key,
|
41
|
-
"secret_key": secret_key,
|
42
|
-
}
|
43
|
-
self.cognito_client_id_token: str = ""
|
44
|
-
self.expiry_time: float = 0
|
45
|
-
|
46
|
-
self.obtain_credentials()
|
47
|
-
|
48
|
-
self.refreshable_credentials = RefreshableCredentials.create_from_metadata(
|
49
|
-
metadata=self.get_current_credentials(),
|
50
|
-
refresh_using=self.refresh_credentials,
|
51
|
-
method="custom-refresh",
|
52
|
-
)
|
53
|
-
|
54
|
-
botocore_session = BotocoreSession()
|
55
|
-
botocore_session._credentials = self.refreshable_credentials # type: ignore
|
56
|
-
botocore_session.set_config_variable("signature_version", "v4")
|
57
|
-
|
58
|
-
self.session = boto3.Session(botocore_session=botocore_session)
|
59
|
-
logger.debug("Boto3 session created with RefreshableCredentials.")
|
60
|
-
|
61
|
-
def obtain_credentials(self) -> None:
|
62
|
-
payload = {}
|
63
|
-
if self.username and self.password:
|
64
|
-
payload = {"username": self.username, "password": self.password}
|
65
|
-
elif self.user_sub_id and self.refresh_token:
|
66
|
-
payload = {"user_sub_id": self.user_sub_id, "refresh_token": self.refresh_token}
|
67
|
-
else:
|
68
|
-
raise CredentialError("Insufficient credentials to obtain AWS session.")
|
69
|
-
|
70
|
-
try:
|
71
|
-
response = requests.post(
|
72
|
-
f"{self.lambda_url}/auth/login",
|
73
|
-
json=payload,
|
74
|
-
headers={"X-Client-Version": __version__},
|
75
|
-
)
|
76
|
-
response.raise_for_status()
|
77
|
-
data = response.json()
|
78
|
-
|
79
|
-
token_key = next(
|
80
|
-
(key for key in ["cognito_client_id_token", "id_token", "access_token"] if key in data), None
|
81
|
-
)
|
82
|
-
|
83
|
-
if not token_key:
|
84
|
-
raise CredentialError("No valid token found in authentication response.")
|
85
|
-
|
86
|
-
self.credentials = {
|
87
|
-
"access_key": data.get("aws_access_key_id"),
|
88
|
-
"secret_key": data.get("aws_secret_access_key"),
|
89
|
-
"token": data.get("aws_session_token"),
|
90
|
-
}
|
91
|
-
|
92
|
-
token = data.get(token_key)
|
93
|
-
if not token:
|
94
|
-
raise CredentialError(f"Token '{token_key}' is missing in the authentication response.")
|
95
|
-
|
96
|
-
decoded = jwt.decode(token, options={"verify_signature": False})
|
97
|
-
self.expiry_time = decoded.get("exp", time.time() + 3600) - 60 # Refresh 1 minute before expiry
|
98
|
-
|
99
|
-
self.cognito_client_id_token = token
|
100
|
-
|
101
|
-
except requests.exceptions.HTTPError as http_err:
|
102
|
-
response = http_err.response
|
103
|
-
if response is None:
|
104
|
-
raise CredentialError("Response was None?!") from http_err
|
105
|
-
|
106
|
-
if response.status_code == 401:
|
107
|
-
raise CredentialError("Incorrect username or password.") from http_err
|
108
|
-
else:
|
109
|
-
if js := response.json():
|
110
|
-
if message := js.get("Message"):
|
111
|
-
raise CredentialError(message)
|
112
|
-
error_info = f"HTTP {response.status_code} {response.reason}"
|
113
|
-
raise CredentialError(f"HTTP error occurred while obtaining credentials: {error_info}") from http_err
|
114
|
-
except requests.exceptions.RequestException as req_err:
|
115
|
-
raise CredentialError(f"Request exception occurred while obtaining credentials: {req_err}") from req_err
|
116
|
-
except jwt.DecodeError as decode_err:
|
117
|
-
raise CredentialError(f"Failed to decode JWT token: {decode_err}") from decode_err
|
118
|
-
except KeyError as key_err:
|
119
|
-
raise CredentialError(f"Missing expected key in authentication response: {key_err}") from key_err
|
120
|
-
|
121
|
-
def refresh_credentials(self) -> dict[str, Any]:
|
122
|
-
logger.debug("Refreshing credentials using refresh_credentials method.")
|
123
|
-
self.obtain_credentials()
|
124
|
-
return {
|
125
|
-
"access_key": self.credentials.get("access_key"),
|
126
|
-
"secret_key": self.credentials.get("secret_key"),
|
127
|
-
"token": self.credentials.get("token"),
|
128
|
-
"expiry_time": datetime.fromtimestamp(self.expiry_time, tz=UTC).isoformat(),
|
129
|
-
}
|
130
|
-
|
131
|
-
def get_current_credentials(self) -> dict[str, Any]:
|
132
|
-
if not self.cognito_client_id_token:
|
133
|
-
raise CredentialError("cognito_client_id_token not set in CredentialManager.")
|
134
|
-
|
135
|
-
return {
|
136
|
-
"access_key": self.credentials.get("access_key"),
|
137
|
-
"secret_key": self.credentials.get("secret_key"),
|
138
|
-
"token": self.credentials.get("token"),
|
139
|
-
"expiry_time": datetime.fromtimestamp(self.expiry_time, tz=UTC).isoformat(),
|
140
|
-
"cognito_client_id_token": self.cognito_client_id_token,
|
141
|
-
}
|
locust_cloud/idle_exit.py
DELETED
@@ -1,38 +0,0 @@
|
|
1
|
-
import logging
|
2
|
-
import sys
|
3
|
-
|
4
|
-
import gevent
|
5
|
-
import locust.env
|
6
|
-
from locust import events
|
7
|
-
|
8
|
-
logger = logging.getLogger(__name__)
|
9
|
-
|
10
|
-
|
11
|
-
class IdleExit:
|
12
|
-
def __init__(self, environment: locust.env.Environment):
|
13
|
-
self.environment = environment
|
14
|
-
self._destroy_task: gevent.Greenlet | None = None
|
15
|
-
events.test_start.add_listener(self.on_locust_state_change)
|
16
|
-
events.test_stop.add_listener(self.on_test_stop)
|
17
|
-
events.quit.add_listener(self.on_locust_state_change)
|
18
|
-
|
19
|
-
if not self.environment.parsed_options.autostart:
|
20
|
-
self._destroy_task = gevent.spawn(self._destroy)
|
21
|
-
|
22
|
-
def _destroy(self):
|
23
|
-
gevent.sleep(1800)
|
24
|
-
logger.info("Locust was detected as idle (no test running) for more than 30 minutes")
|
25
|
-
self.environment.runner.quit()
|
26
|
-
|
27
|
-
if self.environment.web_ui:
|
28
|
-
self.environment.web_ui.greenlet.kill(timeout=5)
|
29
|
-
|
30
|
-
if self.environment.web_ui.greenlet.started:
|
31
|
-
sys.exit(1)
|
32
|
-
|
33
|
-
def on_test_stop(self, **kwargs):
|
34
|
-
self._destroy_task = gevent.spawn(self._destroy)
|
35
|
-
|
36
|
-
def on_locust_state_change(self, **kwargs):
|
37
|
-
if self._destroy_task:
|
38
|
-
self._destroy_task.kill()
|