locust-cloud 1.8.0__py3-none-any.whl → 1.9.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/auth.py +177 -14
- locust_cloud/cloud.py +27 -50
- locust_cloud/credential_manager.py +5 -2
- locust_cloud/timescale/query.py +10 -4
- locust_cloud/webui/.eslintrc +1 -1
- locust_cloud/webui/dist/assets/{index-DQyGe4ep.js → index-DVpRWzFO.js} +89 -89
- locust_cloud/webui/dist/index.html +1 -1
- locust_cloud/webui/package.json +2 -2
- locust_cloud/webui/tsconfig.tsbuildinfo +1 -1
- locust_cloud/webui/yarn.lock +6 -131
- {locust_cloud-1.8.0.dist-info → locust_cloud-1.9.0.dist-info}/METADATA +2 -2
- locust_cloud-1.9.0.dist-info/RECORD +23 -0
- locust_cloud/constants.py +0 -5
- locust_cloud-1.8.0.dist-info/RECORD +0 -24
- {locust_cloud-1.8.0.dist-info → locust_cloud-1.9.0.dist-info}/WHEEL +0 -0
- {locust_cloud-1.8.0.dist-info → locust_cloud-1.9.0.dist-info}/entry_points.txt +0 -0
locust_cloud/auth.py
CHANGED
@@ -1,17 +1,20 @@
|
|
1
1
|
import logging
|
2
2
|
import os
|
3
3
|
from datetime import UTC, datetime, timedelta
|
4
|
-
from typing import TypedDict
|
4
|
+
from typing import Any, TypedDict, cast
|
5
5
|
|
6
6
|
import locust.env
|
7
7
|
import requests
|
8
8
|
import werkzeug
|
9
|
-
from flask import redirect, request, url_for
|
9
|
+
from flask import Blueprint, redirect, request, session, url_for
|
10
10
|
from flask_login import UserMixin, login_user
|
11
|
+
from locust.html import render_template_from
|
11
12
|
from locust_cloud import __version__
|
12
|
-
from locust_cloud.constants import DEFAULT_DEPLOYER_URL
|
13
13
|
|
14
|
-
|
14
|
+
REGION = os.environ.get("AWS_DEFAULT_REGION")
|
15
|
+
DEPLOYER_URL = f"https://api.{REGION}.locust.cloud/1"
|
16
|
+
ALLOW_SIGNUP = os.environ.get("ALLOW_SIGNUP", True)
|
17
|
+
|
15
18
|
|
16
19
|
logger = logging.getLogger(__name__)
|
17
20
|
|
@@ -47,6 +50,9 @@ def register_auth(environment: locust.env.Environment):
|
|
47
50
|
environment.web_ui.app.config["SECRET_KEY"] = os.getenv("SECRET_KEY")
|
48
51
|
environment.web_ui.app.debug = False
|
49
52
|
|
53
|
+
web_base_path = environment.parsed_options.web_base_path
|
54
|
+
auth_blueprint = Blueprint("locust_cloud_auth", __name__, url_prefix=web_base_path)
|
55
|
+
|
50
56
|
def load_user(user_sub_id: str):
|
51
57
|
username = request.cookies.get("username")
|
52
58
|
refresh_token = request.cookies.get("user_token")
|
@@ -58,11 +64,19 @@ def register_auth(environment: locust.env.Environment):
|
|
58
64
|
return None
|
59
65
|
|
60
66
|
environment.web_ui.login_manager.user_loader(load_user)
|
61
|
-
environment.web_ui.auth_args =
|
62
|
-
|
63
|
-
|
67
|
+
environment.web_ui.auth_args = cast(
|
68
|
+
Any,
|
69
|
+
{
|
70
|
+
"username_password_callback": f"{web_base_path}/authenticate",
|
71
|
+
},
|
72
|
+
)
|
73
|
+
|
74
|
+
if ALLOW_SIGNUP:
|
75
|
+
environment.web_ui.auth_args["auth_providers"] = [
|
76
|
+
{"label": "Sign Up", "callback_url": f"{web_base_path}/signup"}
|
77
|
+
]
|
64
78
|
|
65
|
-
@
|
79
|
+
@auth_blueprint.route("/authenticate", methods=["POST"])
|
66
80
|
def login_submit():
|
67
81
|
username = request.form.get("username", "")
|
68
82
|
password = request.form.get("password")
|
@@ -77,19 +91,168 @@ def register_auth(environment: locust.env.Environment):
|
|
77
91
|
auth_response.raise_for_status()
|
78
92
|
|
79
93
|
credentials = auth_response.json()
|
80
|
-
response = redirect(url_for("index"))
|
94
|
+
response = redirect(url_for("locust.index"))
|
81
95
|
response = set_credentials(username, credentials, response)
|
82
96
|
login_user(AuthUser(credentials["user_sub_id"]))
|
83
97
|
|
84
98
|
return response
|
85
99
|
except requests.exceptions.HTTPError as e:
|
86
100
|
if e.response.status_code == 401:
|
87
|
-
|
101
|
+
session["auth_error"] = "Invalid username or password"
|
88
102
|
else:
|
89
103
|
logger.error(f"Unknown response from auth: {e.response.status_code} {e.response.text}")
|
90
104
|
|
91
|
-
|
92
|
-
|
93
|
-
|
105
|
+
session["auth_error"] = "Unknown error during authentication, check logs and/or contact support"
|
106
|
+
|
107
|
+
return redirect(url_for("locust.login"))
|
108
|
+
|
109
|
+
@auth_blueprint.route("/signup")
|
110
|
+
def signup():
|
111
|
+
if not ALLOW_SIGNUP:
|
112
|
+
return redirect(url_for("locust.login"))
|
113
|
+
|
114
|
+
if session.get("username"):
|
115
|
+
sign_up_args = {
|
116
|
+
"custom_form": {
|
117
|
+
"inputs": [
|
118
|
+
{
|
119
|
+
"label": "Confirmation Code",
|
120
|
+
"name": "confirmation_code",
|
121
|
+
},
|
122
|
+
],
|
123
|
+
"callback_url": f"{web_base_path}/confirm-signup",
|
124
|
+
"submit_button_text": "Confirm Email",
|
125
|
+
},
|
126
|
+
}
|
127
|
+
else:
|
128
|
+
sign_up_args = {
|
129
|
+
"custom_form": {
|
130
|
+
"inputs": [
|
131
|
+
{
|
132
|
+
"label": "Username",
|
133
|
+
"name": "username",
|
134
|
+
},
|
135
|
+
{
|
136
|
+
"label": "Password",
|
137
|
+
"name": "password",
|
138
|
+
"is_secret": True,
|
139
|
+
},
|
140
|
+
{
|
141
|
+
"label": "Access Code",
|
142
|
+
"name": "access_code",
|
143
|
+
},
|
144
|
+
{
|
145
|
+
"label": "I consent to:\n\n1. Only test your own website/service or our example 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.",
|
146
|
+
"name": "consent",
|
147
|
+
"default_value": False,
|
148
|
+
},
|
149
|
+
],
|
150
|
+
"callback_url": f"{web_base_path}/create-account",
|
151
|
+
"submit_button_text": "Sign Up",
|
152
|
+
},
|
153
|
+
}
|
154
|
+
|
155
|
+
if session.get("auth_info"):
|
156
|
+
sign_up_args["info"] = session["auth_info"]
|
157
|
+
if session.get("auth_sign_up_error"):
|
158
|
+
sign_up_args["error"] = session["auth_sign_up_error"]
|
159
|
+
|
160
|
+
return render_template_from(
|
161
|
+
"auth.html",
|
162
|
+
auth_args=sign_up_args,
|
163
|
+
)
|
164
|
+
|
165
|
+
@auth_blueprint.route("/create-account", methods=["POST"])
|
166
|
+
def create_account():
|
167
|
+
if not ALLOW_SIGNUP:
|
168
|
+
return redirect(url_for("locust.login"))
|
169
|
+
|
170
|
+
session["auth_sign_up_error"] = ""
|
171
|
+
session["auth_info"] = ""
|
172
|
+
|
173
|
+
username = request.form.get("username", "")
|
174
|
+
password = request.form.get("password")
|
175
|
+
access_code = request.form.get("access_code")
|
176
|
+
has_consented = request.form.get("consent")
|
177
|
+
|
178
|
+
if not has_consented:
|
179
|
+
session["auth_sign_up_error"] = "Please accept the terms and conditions to create an account."
|
180
|
+
|
181
|
+
return redirect(url_for("locust_cloud_auth.signup"))
|
182
|
+
|
183
|
+
try:
|
184
|
+
auth_response = requests.post(
|
185
|
+
f"{DEPLOYER_URL}/auth/signup",
|
186
|
+
json={"username": username, "password": password, "access_code": access_code},
|
187
|
+
)
|
188
|
+
|
189
|
+
auth_response.raise_for_status()
|
190
|
+
|
191
|
+
session["user_sub"] = auth_response.json().get("user_sub")
|
192
|
+
session["username"] = username
|
193
|
+
session["auth_info"] = (
|
194
|
+
"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)"
|
195
|
+
)
|
196
|
+
|
197
|
+
return redirect(url_for("locust_cloud_auth.signup"))
|
198
|
+
except requests.exceptions.HTTPError as e:
|
199
|
+
message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
|
200
|
+
session["auth_info"] = ""
|
201
|
+
session["auth_sign_up_error"] = message
|
202
|
+
|
203
|
+
return redirect(url_for("locust_cloud_auth.signup"))
|
204
|
+
|
205
|
+
@auth_blueprint.route("/resend-code")
|
206
|
+
def resend_code():
|
207
|
+
try:
|
208
|
+
auth_response = requests.post(
|
209
|
+
"http://localhost:8000/1/auth/resend-confirmation",
|
210
|
+
json={"username": session["username"]},
|
211
|
+
)
|
212
|
+
|
213
|
+
auth_response.raise_for_status()
|
214
|
+
|
215
|
+
session["auth_sign_up_error"] = ""
|
216
|
+
session["auth_info"] = "Confirmation code sent, please check your email."
|
217
|
+
|
218
|
+
return redirect(url_for("locust_cloud_auth.signup"))
|
219
|
+
except requests.exceptions.HTTPError as e:
|
220
|
+
message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
|
221
|
+
session["auth_info"] = ""
|
222
|
+
session["auth_sign_up_error"] = message
|
223
|
+
|
224
|
+
return redirect(url_for("locust_cloud_auth.signup"))
|
225
|
+
|
226
|
+
@auth_blueprint.route("/confirm-signup", methods=["POST"])
|
227
|
+
def confirm_signup():
|
228
|
+
if not ALLOW_SIGNUP:
|
229
|
+
return redirect(url_for("locust.login"))
|
230
|
+
|
231
|
+
session["auth_sign_up_error"] = ""
|
232
|
+
confirmation_code = request.form.get("confirmation_code")
|
233
|
+
|
234
|
+
try:
|
235
|
+
auth_response = requests.post(
|
236
|
+
f"{DEPLOYER_URL}/auth/confirm-signup",
|
237
|
+
json={
|
238
|
+
"username": session.get("username"),
|
239
|
+
"user_sub": session["user_sub"],
|
240
|
+
"confirmation_code": confirmation_code,
|
241
|
+
},
|
242
|
+
)
|
243
|
+
|
244
|
+
auth_response.raise_for_status()
|
245
|
+
|
246
|
+
session["username"] = None
|
247
|
+
session["auth_info"] = "Account created successfully!"
|
248
|
+
session["auth_sign_up_error"] = ""
|
249
|
+
|
250
|
+
return redirect(url_for("locust.login"))
|
251
|
+
except requests.exceptions.HTTPError as e:
|
252
|
+
message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
|
253
|
+
session["auth_info"] = ""
|
254
|
+
session["auth_sign_up_error"] = message
|
255
|
+
|
256
|
+
return redirect(url_for("locust_cloud_auth.signup"))
|
94
257
|
|
95
|
-
|
258
|
+
environment.web_ui.app.register_blueprint(auth_blueprint)
|
locust_cloud/cloud.py
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
import json
|
2
2
|
import logging
|
3
|
-
import math
|
4
3
|
import os
|
5
4
|
import sys
|
6
5
|
import time
|
@@ -14,13 +13,6 @@ import configargparse
|
|
14
13
|
import requests
|
15
14
|
from botocore.exceptions import ClientError
|
16
15
|
from locust_cloud import __version__
|
17
|
-
from locust_cloud.constants import (
|
18
|
-
DEFAULT_CLUSTER_NAME,
|
19
|
-
DEFAULT_DEPLOYER_URL,
|
20
|
-
DEFAULT_NAMESPACE,
|
21
|
-
DEFAULT_REGION_NAME,
|
22
|
-
USERS_PER_WORKER,
|
23
|
-
)
|
24
16
|
from locust_cloud.credential_manager import CredentialError, CredentialManager
|
25
17
|
|
26
18
|
|
@@ -110,29 +102,9 @@ advanced.add_argument(
|
|
110
102
|
advanced.add_argument(
|
111
103
|
"--region",
|
112
104
|
type=str,
|
113
|
-
default=os.environ.get("AWS_DEFAULT_REGION"
|
105
|
+
default=os.environ.get("AWS_DEFAULT_REGION"),
|
114
106
|
help="Sets the AWS region to use for the deployed cluster, e.g. us-east-1. It defaults to use AWS_DEFAULT_REGION env var, like AWS tools.",
|
115
107
|
)
|
116
|
-
advanced.add_argument(
|
117
|
-
"--kube-cluster-name",
|
118
|
-
type=str,
|
119
|
-
default=DEFAULT_CLUSTER_NAME,
|
120
|
-
help=configargparse.SUPPRESS,
|
121
|
-
env_var="KUBE_CLUSTER_NAME",
|
122
|
-
)
|
123
|
-
advanced.add_argument(
|
124
|
-
"--kube-namespace",
|
125
|
-
type=str,
|
126
|
-
default=DEFAULT_NAMESPACE,
|
127
|
-
help=configargparse.SUPPRESS,
|
128
|
-
env_var="KUBE_NAMESPACE",
|
129
|
-
)
|
130
|
-
parser.add_argument(
|
131
|
-
"--deployer-url",
|
132
|
-
type=str,
|
133
|
-
default=DEFAULT_DEPLOYER_URL,
|
134
|
-
help=configargparse.SUPPRESS,
|
135
|
-
)
|
136
108
|
parser.add_argument(
|
137
109
|
"--aws-access-key-id",
|
138
110
|
type=str,
|
@@ -162,7 +134,7 @@ parser.add_argument(
|
|
162
134
|
parser.add_argument(
|
163
135
|
"--workers",
|
164
136
|
type=int,
|
165
|
-
help=
|
137
|
+
help="Number of workers to use for the deployment. Defaults to number of users divided by 500, but the default may be customized for your account.",
|
166
138
|
default=None,
|
167
139
|
)
|
168
140
|
parser.add_argument(
|
@@ -194,25 +166,30 @@ logging.getLogger("requests").setLevel(logging.INFO)
|
|
194
166
|
logging.getLogger("urllib3").setLevel(logging.INFO)
|
195
167
|
|
196
168
|
|
169
|
+
api_url = f"https://api.{options.region}.locust.cloud/1"
|
170
|
+
|
171
|
+
|
197
172
|
def main() -> None:
|
198
|
-
|
173
|
+
if not options.region:
|
174
|
+
logger.error(
|
175
|
+
"Setting a region is required to use Locust Cloud. Please ensure the AWS_DEFAULT_REGION env variable or the --region flag is set."
|
176
|
+
)
|
177
|
+
sys.exit(1)
|
178
|
+
|
179
|
+
s3_bucket = "dmdb-default" if options.region == "us-east-1" else "locust-default"
|
199
180
|
deployments: list[Any] = []
|
200
|
-
worker_count: int = max(options.workers or math.ceil(options.users / USERS_PER_WORKER), 2)
|
201
|
-
os.environ["AWS_DEFAULT_REGION"] = options.region
|
202
181
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
)
|
207
|
-
|
208
|
-
"Authentication is required to use Locust Cloud. Please ensure the LOCUST_CLOUD_USERNAME and LOCUST_CLOUD_PASSWORD environment variables are set."
|
209
|
-
)
|
210
|
-
sys.exit(1)
|
182
|
+
if not ((options.username and options.password) or (options.aws_access_key_id and options.aws_secret_access_key)):
|
183
|
+
logger.error(
|
184
|
+
"Authentication is required to use Locust Cloud. Please ensure the LOCUST_CLOUD_USERNAME and LOCUST_CLOUD_PASSWORD environment variables are set."
|
185
|
+
)
|
186
|
+
sys.exit(1)
|
211
187
|
|
212
|
-
|
213
|
-
logger.
|
188
|
+
try:
|
189
|
+
logger.info(f"Authenticating ({options.region}, v{__version__})")
|
190
|
+
logger.debug(f"Lambda url: {api_url}")
|
214
191
|
credential_manager = CredentialManager(
|
215
|
-
lambda_url=
|
192
|
+
lambda_url=api_url,
|
216
193
|
access_key=options.aws_access_key_id,
|
217
194
|
secret_key=options.aws_secret_access_key,
|
218
195
|
username=options.username,
|
@@ -279,20 +256,21 @@ def main() -> None:
|
|
279
256
|
]
|
280
257
|
and os.environ[env_variable]
|
281
258
|
]
|
282
|
-
deploy_endpoint = f"{
|
259
|
+
deploy_endpoint = f"{api_url}/deploy"
|
283
260
|
payload = {
|
284
261
|
"locust_args": [
|
285
262
|
{"name": "LOCUST_LOCUSTFILE", "value": locustfile_url},
|
286
263
|
{"name": "LOCUST_USERS", "value": str(options.users)},
|
287
264
|
{"name": "LOCUST_FLAGS", "value": " ".join(locust_options)},
|
288
265
|
{"name": "LOCUSTCLOUD_REQUIREMENTS_URL", "value": requirements_url},
|
289
|
-
{"name": "LOCUSTCLOUD_DEPLOYER_URL", "value":
|
266
|
+
{"name": "LOCUSTCLOUD_DEPLOYER_URL", "value": api_url},
|
290
267
|
*locust_env_variables,
|
291
268
|
],
|
292
|
-
"worker_count": worker_count,
|
293
269
|
"user_count": options.users,
|
294
270
|
"image_tag": options.image_tag,
|
295
271
|
}
|
272
|
+
if options.workers is not None:
|
273
|
+
payload["worker_count"] = options.workers
|
296
274
|
headers = {
|
297
275
|
"Authorization": f"Bearer {cognito_client_id_token}",
|
298
276
|
"Content-Type": "application/json",
|
@@ -325,7 +303,7 @@ def main() -> None:
|
|
325
303
|
logger.debug("Interrupted by user")
|
326
304
|
sys.exit(0)
|
327
305
|
|
328
|
-
log_group_name =
|
306
|
+
log_group_name = "/eks/dmdb-default" if options.region == "us-east-1" else "/eks/locust-default"
|
329
307
|
master_pod_name = next((deployment for deployment in deployments if "master" in deployment), None)
|
330
308
|
|
331
309
|
if not master_pod_name:
|
@@ -420,9 +398,8 @@ def delete(s3_bucket, credential_manager):
|
|
420
398
|
headers["AWS_SESSION_TOKEN"] = token
|
421
399
|
|
422
400
|
response = requests.delete(
|
423
|
-
f"{
|
401
|
+
f"{api_url}/teardown",
|
424
402
|
headers=headers,
|
425
|
-
params={"namespace": options.kube_namespace} if options.kube_namespace else {},
|
426
403
|
)
|
427
404
|
|
428
405
|
if response.status_code != 200:
|
@@ -100,13 +100,16 @@ class CredentialManager:
|
|
100
100
|
|
101
101
|
except requests.exceptions.HTTPError as http_err:
|
102
102
|
response = http_err.response
|
103
|
-
if response is
|
103
|
+
if response is None:
|
104
|
+
raise CredentialError("Response was None?!") from http_err
|
105
|
+
|
106
|
+
if response.status_code == 401:
|
104
107
|
raise CredentialError("Incorrect username or password.") from http_err
|
105
108
|
else:
|
106
109
|
if js := response.json():
|
107
110
|
if message := js.get("Message"):
|
108
111
|
raise CredentialError(message)
|
109
|
-
error_info = f"HTTP {response.status_code} {response.reason}"
|
112
|
+
error_info = f"HTTP {response.status_code} {response.reason}"
|
110
113
|
raise CredentialError(f"HTTP error occurred while obtaining credentials: {error_info}") from http_err
|
111
114
|
except requests.exceptions.RequestException as req_err:
|
112
115
|
raise CredentialError(f"Request exception occurred while obtaining credentials: {req_err}") from req_err
|
locust_cloud/timescale/query.py
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
import logging
|
2
2
|
|
3
|
-
from flask import make_response, request
|
3
|
+
from flask import Blueprint, make_response, request
|
4
4
|
from flask_login import login_required
|
5
5
|
from locust_cloud.timescale.queries import queries
|
6
6
|
|
@@ -12,7 +12,11 @@ def adapt_timestamp(result):
|
|
12
12
|
|
13
13
|
|
14
14
|
def register_query(environment, pool):
|
15
|
-
|
15
|
+
cloud_stats_blueprint = Blueprint(
|
16
|
+
"locust_cloud_stats", __name__, url_prefix=environment.parsed_options.web_base_path
|
17
|
+
)
|
18
|
+
|
19
|
+
@cloud_stats_blueprint.route("/cloud-stats/<query>", methods=["POST"])
|
16
20
|
@login_required
|
17
21
|
def query(query):
|
18
22
|
results = []
|
@@ -57,7 +61,9 @@ def register_query(environment, pool):
|
|
57
61
|
return results
|
58
62
|
else:
|
59
63
|
logger.warning(f"Received invalid query key: '{query}'")
|
60
|
-
return make_response("Invalid query key", 401)
|
64
|
+
return make_response({"error": "Invalid query key"}, 401)
|
61
65
|
except Exception as e:
|
62
66
|
logger.info(f"Error executing UI query '{query}': {e}", exc_info=True)
|
63
|
-
return make_response("Error executing query", 401)
|
67
|
+
return make_response({"error": "Error executing query"}, 401)
|
68
|
+
|
69
|
+
environment.web_ui.app.register_blueprint(cloud_stats_blueprint)
|