Skip to content

Commit 15472c9

Browse files
authored
Merge branch 'main' into wip/support-us-gov-east-1
2 parents 149c6f4 + e833245 commit 15472c9

File tree

67 files changed

+2107
-446
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+2107
-446
lines changed

.github/workflows/demo_deploy.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ concurrency: ci-${{ github.ref }}
1313

1414
jobs:
1515
frontend-tests:
16-
runs-on: ubuntu-20.04
16+
runs-on: ubuntu-latest
1717

1818
steps:
1919
- name: Checkout repo
@@ -43,7 +43,7 @@ jobs:
4343
working-directory: ./frontend
4444

4545
backend-tests:
46-
runs-on: ubuntu-20.04
46+
runs-on: ubuntu-latest
4747

4848
steps:
4949
- name: Checkout repo
@@ -62,7 +62,7 @@ jobs:
6262
run: pytest
6363

6464
update-infrastructure:
65-
runs-on: ubuntu-20.04
65+
runs-on: ubuntu-latest
6666

6767
needs: [frontend-tests, backend-tests]
6868

@@ -80,7 +80,7 @@ jobs:
8080
run: ./infrastructure/update-environment-infra.sh demo
8181

8282
build-and-deploy:
83-
runs-on: ubuntu-20.04
83+
runs-on: ubuntu-latest
8484

8585
needs: [update-infrastructure]
8686

.github/workflows/production_release.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ permissions:
1010

1111
jobs:
1212
frontend-tests:
13-
runs-on: ubuntu-20.04
13+
runs-on: ubuntu-latest
1414

1515
steps:
1616
- name: Checkout repo
@@ -40,7 +40,7 @@ jobs:
4040
working-directory: ./frontend
4141

4242
backend-tests:
43-
runs-on: ubuntu-20.04
43+
runs-on: ubuntu-latest
4444

4545
steps:
4646
- name: Checkout repo
@@ -59,7 +59,7 @@ jobs:
5959
run: pytest
6060

6161
release:
62-
runs-on: ubuntu-20.04
62+
runs-on: ubuntu-latest
6363

6464
needs: [frontend-tests, backend-tests]
6565

.github/workflows/pull_request.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77

88
jobs:
99
frontend-tests:
10-
runs-on: ubuntu-20.04
10+
runs-on: ubuntu-latest
1111

1212
steps:
1313
- name: Checkout repo
@@ -37,7 +37,7 @@ jobs:
3737
working-directory: ./frontend
3838

3939
backend-tests:
40-
runs-on: ubuntu-20.04
40+
runs-on: ubuntu-latest
4141

4242
steps:
4343
- name: Checkout repo

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM tiangolo/uwsgi-nginx-flask:python3.8-alpine
1+
FROM tiangolo/uwsgi-nginx-flask:python3.12-alpine
22

33
ENV PYTHONUNBUFFERED=1
44
ENV STATIC_URL /static

Dockerfile.awslambda

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM public.ecr.aws/lambda/python:3.8-x86_64
1+
FROM public.ecr.aws/lambda/python:3.12-x86_64
22

33
COPY --from=frontend-awslambda /app/build ${LAMBDA_TASK_ROOT}/frontend/public
44

api/PclusterApiHandler.py

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,21 @@
2525
from api.pcm_globals import set_auth_cookies_in_context, logger, auth_cookies
2626
from api.security.csrf.constants import CSRF_COOKIE_NAME
2727
from api.security.csrf.csrf import csrf_needed
28-
from api.utils import disable_auth
28+
from api.utils import disable_auth, read_and_delete_ssm_output_from_cloudwatch
2929
from api.validation import validated
3030
from api.validation.schemas import PCProxyArgs, PCProxyBody
3131

3232
USER_POOL_ID = os.getenv("USER_POOL_ID")
3333
AUTH_PATH = os.getenv("AUTH_PATH")
3434
API_BASE_URL = os.getenv("API_BASE_URL")
35-
API_VERSION = os.getenv("API_VERSION", "3.1.0")
35+
API_VERSION = sorted(set(os.getenv("API_VERSION", "3.1.0").strip().split(",")), key=lambda x: [-int(n) for n in x.split('.')])
36+
# Default version must be highest version so that it can be used for read operations due to backwards compatibility
37+
DEFAULT_API_VERSION = API_VERSION[0]
3638
API_USER_ROLE = os.getenv("API_USER_ROLE")
3739
OIDC_PROVIDER = os.getenv("OIDC_PROVIDER")
3840
CLIENT_ID = os.getenv("CLIENT_ID")
3941
CLIENT_SECRET = os.getenv("CLIENT_SECRET")
4042
SECRET_ID = os.getenv("SECRET_ID")
41-
SITE_URL = os.getenv("SITE_URL", API_BASE_URL)
4243
SCOPES_LIST = os.getenv("SCOPES_LIST")
4344
REGION = os.getenv("AWS_DEFAULT_REGION")
4445
TOKEN_URL = os.getenv("TOKEN_URL", f"{AUTH_PATH}/oauth2/token")
@@ -47,6 +48,8 @@
4748
JWKS_URL = os.getenv("JWKS_URL")
4849
AUDIENCE = os.getenv("AUDIENCE")
4950
USER_ROLES_CLAIM = os.getenv("USER_ROLES_CLAIM", "cognito:groups")
51+
SSM_LOG_GROUP_NAME = os.getenv("SSM_LOG_GROUP_NAME")
52+
ARG_VERSION="version"
5053

5154
try:
5255
if (not USER_POOL_ID or USER_POOL_ID == "") and SECRET_ID:
@@ -62,6 +65,19 @@
6265
JWKS_URL = os.getenv("JWKS_URL",
6366
f"https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}/" ".well-known/jwks.json")
6467

68+
def create_url_map(url_list):
69+
url_map = {}
70+
if url_list:
71+
for url in url_list.split(","):
72+
if url:
73+
pair=url.split("=")
74+
url_map[pair[0]] = pair[1]
75+
return url_map
76+
77+
API_BASE_URL_MAPPING = create_url_map(API_BASE_URL)
78+
SITE_URL = os.getenv("SITE_URL", API_BASE_URL_MAPPING.get(DEFAULT_API_VERSION))
79+
80+
6581

6682
def jwt_decode(token, audience=None, access_token=None):
6783
return jwt.decode(
@@ -164,7 +180,7 @@ def authenticate(groups):
164180

165181
if (not groups):
166182
return abort(403)
167-
183+
168184
jwt_roles = set(decoded.get(USER_ROLES_CLAIM, []))
169185
groups_granted = groups.intersection(jwt_roles)
170186
if len(groups_granted) == 0:
@@ -190,7 +206,7 @@ def get_scopes_list():
190206

191207
def get_redirect_uri():
192208
return f"{SITE_URL}/login"
193-
209+
194210
# Local Endpoints
195211

196212

@@ -232,9 +248,9 @@ def ec2_action():
232248
def get_cluster_config_text(cluster_name, region=None):
233249
url = f"/v3/clusters/{cluster_name}"
234250
if region:
235-
info_resp = sigv4_request("GET", API_BASE_URL, url, params={"region": region})
251+
info_resp = sigv4_request("GET", get_base_url(request), url, params={"region": region})
236252
else:
237-
info_resp = sigv4_request("GET", API_BASE_URL, url)
253+
info_resp = sigv4_request("GET", get_base_url(request), url)
238254
if info_resp.status_code != 200:
239255
abort(info_resp.status_code)
240256

@@ -264,10 +280,16 @@ def ssm_command(region, instance_id, user, run_command):
264280
DocumentName="AWS-RunShellScript",
265281
Comment=f"Run ssm command.",
266282
Parameters={"commands": [command]},
283+
CloudWatchOutputConfig={
284+
'CloudWatchLogGroupName': SSM_LOG_GROUP_NAME,
285+
'CloudWatchOutputEnabled': True
286+
},
267287
)
268288

269289
command_id = ssm_resp["Command"]["CommandId"]
270290

291+
logger.info(f"Submitted SSM command {command_id}")
292+
271293
# Wait for command to complete
272294
time.sleep(0.75)
273295
while time.time() - start < 60:
@@ -282,7 +304,13 @@ def ssm_command(region, instance_id, user, run_command):
282304
if status["Status"] != "Success":
283305
raise Exception(status["StandardErrorContent"])
284306

285-
output = status["StandardOutputContent"]
307+
output = read_and_delete_ssm_output_from_cloudwatch(
308+
region=region,
309+
log_group_name=SSM_LOG_GROUP_NAME,
310+
command_id=command_id,
311+
instance_id=instance_id,
312+
)
313+
286314
return output
287315

288316

@@ -352,7 +380,7 @@ def sacct():
352380
user,
353381
f"sacct {sacct_args} --json "
354382
+ "| jq -c .jobs[0:120]\\|\\map\\({name,user,partition,state,job_id,exit_code\\}\\)",
355-
)
383+
)
356384
if type(accounting) is tuple:
357385
return accounting
358386
else:
@@ -471,7 +499,7 @@ def get_dcv_session():
471499

472500

473501
def get_custom_image_config():
474-
image_info = sigv4_request("GET", API_BASE_URL, f"/v3/images/custom/{request.args.get('image_id')}").json()
502+
image_info = sigv4_request("GET", get_base_url(request), f"/v3/images/custom/{request.args.get('image_id')}").json()
475503
configuration = requests.get(image_info["imageConfiguration"]["url"])
476504
return configuration.text
477505

@@ -553,13 +581,7 @@ def get_instance_types():
553581
ec2 = boto3.client("ec2", config=config)
554582
else:
555583
ec2 = boto3.client("ec2")
556-
filters = [
557-
{"Name": "current-generation", "Values": ["true"]},
558-
{"Name": "instance-type",
559-
"Values": [
560-
"c5*", "c6*", "c7*", "g4*", "g5*", "g6*", "hpc*", "p3*", "p4*", "p5*", "t2*", "t3*", "m6*", "m7*", "r*"
561-
]},
562-
]
584+
filters = [{"Name": "current-generation", "Values": ["true"]}]
563585
instance_paginator = ec2.get_paginator("describe_instance_types")
564586
instances_paginator = instance_paginator.paginate(Filters=filters)
565587
instance_types = []
@@ -583,9 +605,9 @@ def _get_identity_from_token(decoded, claims):
583605
identity["username"] = decoded["username"]
584606

585607
for claim in claims:
586-
if claim in decoded:
587-
identity["attributes"][claim] = decoded[claim]
588-
608+
if claim in decoded:
609+
identity["attributes"][claim] = decoded[claim]
610+
589611
return identity
590612

591613
def get_identity():
@@ -722,14 +744,20 @@ def _get_params(_request):
722744
params.pop("path")
723745
return params
724746

747+
def get_base_url(request):
748+
version = request.args.get(ARG_VERSION)
749+
if version and str(version) in API_VERSION:
750+
return API_BASE_URL_MAPPING[str(version)]
751+
return API_BASE_URL_MAPPING[DEFAULT_API_VERSION]
752+
725753

726754
pc = Blueprint('pc', __name__)
727755

728756
@pc.get('/', strict_slashes=False)
729757
@authenticated({'admin'})
730758
@validated(params=PCProxyArgs)
731759
def pc_proxy_get():
732-
response = sigv4_request(request.method, API_BASE_URL, request.args.get("path"), _get_params(request))
760+
response = sigv4_request(request.method, get_base_url(request), request.args.get("path"), _get_params(request))
733761
return response.json(), response.status_code
734762

735763
@pc.route('/', methods=['POST','PUT','PATCH','DELETE'], strict_slashes=False)
@@ -743,5 +771,5 @@ def pc_proxy():
743771
except:
744772
pass
745773

746-
response = sigv4_request(request.method, API_BASE_URL, request.args.get("path"), _get_params(request), body=body)
774+
response = sigv4_request(request.method, get_base_url(request), request.args.get("path"), _get_params(request), body=body)
747775
return response.json(), response.status_code

api/tests/exceptions/test_exception_handlers.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,11 @@ def test_unauthenticated_error_handler(client, app, monkeypatch):
4747
def test_validation_error_handler(client, app, monkeypatch):
4848
with app.test_request_context('/'):
4949
app.preprocess_request()
50-
response, status_code = validation_error_handler(ValidationError('Input validation failed for /manager/ec2_action', data={'field': ['validation-error']}))
50+
response, status_code = validation_error_handler(ValidationError('Input validation failed for requested resource /manager/ec2_action', data={'field': ['validation-error']}))
5151

5252
assert status_code == 400
5353
assert response.json == {
54-
'code': 400, 'message': 'Input validation failed for /manager/ec2_action',
54+
'code': 400, 'message': 'Input validation failed for requested resource /manager/ec2_action',
5555
'validation_errors': {'field': ['validation-error']}
5656
}
5757

api/tests/test_pcluster_api_handler.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
from unittest import mock
2-
from api.PclusterApiHandler import login
2+
from api.PclusterApiHandler import login, get_base_url, create_url_map
3+
4+
class MockRequest:
5+
cookies = {'int_value': 100}
6+
args = {'version': '3.12.0'}
7+
json = {'username': '[email protected]'}
38

49

510
@mock.patch("api.PclusterApiHandler.requests.post")
@@ -27,3 +32,14 @@ def test_login_with_no_access_token_returns_401(mocker, app):
2732
login()
2833

2934
mock_abort.assert_called_once_with(401)
35+
36+
def test_get_base_url(monkeypatch):
37+
monkeypatch.setattr('api.PclusterApiHandler.API_VERSION', ['3.12.0', '3.11.0'])
38+
monkeypatch.setattr('api.PclusterApiHandler.API_BASE_URL', '3.12.0=https://example.com,3.11.0=https://example1.com,')
39+
monkeypatch.setattr('api.PclusterApiHandler.API_BASE_URL_MAPPING', {'3.12.0': 'https://example.com', '3.11.0': 'https://example1.com'})
40+
41+
assert 'https://example.com' == get_base_url(MockRequest())
42+
43+
def test_create_url_map():
44+
assert {'3.12.0': 'https://example.com', '3.11.0': 'https://example1.com'} == create_url_map('3.12.0=https://example.com,3.11.0=https://example1.com,')
45+

0 commit comments

Comments
 (0)