Skip to content

Commit ce7f40e

Browse files
Merge pull request #83 from MITLibraries/IN-1052-add-guardrails-final-runs
In 1052 add guardrails final runs
2 parents e9f56de + 0ca0fc9 commit ce7f40e

16 files changed

+136
-153
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ A Flask application run in AWS Lambda used to process invoices from Alma. Invoic
2727
ALMA_SAP_INVOICES_ECR_IMAGE_NAME=### The name of the ECR image for the Alma SAP Invoices CLI.
2828
ALMA_SAP_INVOICES_ECS_CLUSTER=### The full ARN of the ECS cluster to run the Alma SAP Invoices CLI ECS task.
2929
ALMA_SAP_INVOICES_ECS_TASK_DEFINITION=### The family and revision (formatted as <family>:<revision>) or full ARN of Alma SAP Invoices CLI ECS task.
30+
ALMA_SAP_INVOICES_ECS_GROUPS=### Security group(s) for the Alma SAP Invoices ECS task (formatted as a comma-separated string excluding whitespaces, e.g. "sg-abc123,sg-def456").
31+
ALMA_SAP_INVOICES_ECS_SUBNETS=### Subnet(s) for the Alma SAP Invoices ECS task (formatted as a comma-separated string excluding whitespaces, e.g. "subnet-abc123,subnet-def456").
3032
ALMA_SAP_INVOICES_ECS_NETWORK_CONFIG=### The network configuration for the Alma SAP Invoices ECS task.
3133
ALMA_SAP_INVOICES_CLOUDWATCH_LOG_GROUP=### The name of the log group containing logs from Alma SAP Invoices ECS task runs.
3234
LOGIN_DISABLED=### String variable representing a boolean to disable login (i.e., ignore 'login_required' decorator). Set to 'true' to disable login (recommended for unit testing and Dev); must be set to 'false' for Stage and Prod.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ ignore = [
3434
"ANN102",
3535
"COM812",
3636
"D107",
37+
"G004",
3738
"N812",
3839
"PTH",
3940

tests/conftest.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,12 @@ def _test_env(monkeypatch):
2626
"ALMA_SAP_INVOICES_ECS_CLUSTER",
2727
f"arn:aws:ecs:us-east-1:{ACCOUNT_ID}:cluster/mock-sapinvoices-ecs-test",
2828
)
29+
monkeypatch.setenv("ALMA_SAP_INVOICES_ECS_GROUPS", "sg-abc123")
30+
monkeypatch.setenv("ALMA_SAP_INVOICES_ECS_SUBNETS", "subnet-abc123,subnet-def456")
2931
monkeypatch.setenv(
3032
"ALMA_SAP_INVOICES_ECS_TASK_DEFINITION",
3133
f"arn:aws:ecs:us-east-1:{ACCOUNT_ID}:task-definition/mock-sapinvoices-ecs-test:1",
3234
)
33-
monkeypatch.setenv(
34-
"ALMA_SAP_INVOICES_ECS_NETWORK_CONFIG",
35-
'{"awsvpcConfiguration": {"securityGroups": ["sg-abc123"], "subnets": ["subnet-abc123"]}}',
36-
)
3735
monkeypatch.setenv(
3836
"ALMA_SAP_INVOICES_CLOUDWATCH_LOG_GROUP", "mock-sapinvoices-ecs-test"
3937
)

tests/test_app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def test_app_log_activity_executed_runs_success(
7777
) as mock_ecsclient_review_run:
7878
mock_ecsclient_review_run.return_value = "abc123"
7979
sapinvoices_client.get(
80-
"/process-invoices/run/review", headers=mock_request_headers_oidc_data
80+
"/process-invoices/run/review/execute", headers=mock_request_headers_oidc_data
8181
)
8282
assert (
8383
"Authenticated User executed a 'review' run (task ID = 'abc123')."

tests/test_aws/test_cloudwatch.py

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -42,29 +42,3 @@ def test_cloudwatchlogs_client_get_log_events_raise_error(
4242
match="No log streams found for task id 'DOES_NOT_EXIST'.",
4343
):
4444
assert cloudwatchlogs_client.get_log_events(task_id="DOES_NOT_EXIST")
45-
46-
47-
def test_cloudwatchlogs_client_log_stream_exists_returns_true(
48-
cloudwatchlogs_client,
49-
mock_cloudwatchlogs_log_stream_review_run_task,
50-
):
51-
# Note: Either 'review run' or 'final run' (task_id="abc002") mocked log stream
52-
# fixture will work for this test.
53-
assert cloudwatchlogs_client.log_stream_exists(task_id="abc001") is True
54-
55-
56-
def test_cloudwatchlogs_client_log_stream_exists_returns_false(
57-
cloudwatchlogs_client, mock_cloudwatchlogs_log_group
58-
):
59-
assert cloudwatchlogs_client.log_stream_exists(task_id="DOES_NOT_EXIST") is False
60-
61-
62-
def test_cloudwatchlogs_client_get_log_streams_success(
63-
cloudwatchlogs_client,
64-
mock_cloudwatchlogs_log_stream_review_run_task,
65-
mock_cloudwatchlogs_log_stream_final_run_task,
66-
):
67-
assert cloudwatchlogs_client.get_log_streams() == [
68-
"sapinvoices/mock-sapinvoices-ecs-test/abc001",
69-
"sapinvoices/mock-sapinvoices-ecs-test/abc002",
70-
]

tests/test_aws/test_ecs.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# ruff: noqa: E501
2-
import json
32
import os
43

54
import pytest
@@ -17,9 +16,13 @@ def test_ecs_client_init_success(ecs_client):
1716
assert (
1817
ecs_client.task_definition == os.environ["ALMA_SAP_INVOICES_ECS_TASK_DEFINITION"]
1918
)
20-
assert ecs_client.network_configuration == json.loads(
21-
os.environ["ALMA_SAP_INVOICES_ECS_NETWORK_CONFIG"]
22-
)
19+
assert ecs_client.network_configuration == {
20+
"awsvpcConfiguration": {
21+
"subnets": ["subnet-abc123", "subnet-def456"],
22+
"securityGroups": ["sg-abc123"],
23+
"assignPublicIp": "DISABLED",
24+
}
25+
}
2326
assert ecs_client.container == os.environ["ALMA_SAP_INVOICES_ECR_IMAGE_NAME"]
2427

2528

tests/test_config.py

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import pytest
44

5-
from webapp.exceptions import InvalidNetworkConfigurationError
5+
6+
def test_config_check_required_env_vars_success(config):
7+
config.check_required_env_vars()
68

79

810
def test_config_check_required_env_vars_error(monkeypatch, config):
@@ -50,16 +52,11 @@ def test_config_env_access_error(config):
5052
_ = config.DOES_NOT_EXIST
5153

5254

53-
def test_config_env_access_raise_error_if_network_config_not_json(monkeypatch, config):
54-
monkeypatch.setenv("ALMA_SAP_INVOICES_ECS_NETWORK_CONFIG", "test")
55-
with pytest.raises(InvalidNetworkConfigurationError, match="Expecting value"):
56-
_ = config.ALMA_SAP_INVOICES_ECS_NETWORK_CONFIG
57-
58-
59-
def test_config_env_access_raise_error_if_network_config_is_missing(monkeypatch, config):
60-
monkeypatch.delenv("ALMA_SAP_INVOICES_ECS_NETWORK_CONFIG")
61-
with pytest.raises(
62-
InvalidNetworkConfigurationError,
63-
match="the JSON object must be str, bytes or bytearray, not NoneType",
64-
):
65-
_ = config.ALMA_SAP_INVOICES_ECS_NETWORK_CONFIG
55+
def test_config_env_access_network_config_success(monkeypatch, config):
56+
assert config.ALMA_SAP_INVOICES_ECS_NETWORK_CONFIG == {
57+
"awsvpcConfiguration": {
58+
"subnets": ["subnet-abc123", "subnet-def456"],
59+
"securityGroups": ["sg-abc123"],
60+
"assignPublicIp": "DISABLED",
61+
}
62+
}

webapp/app.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
11
from __future__ import annotations
22

33
import logging
4+
import time
45
from typing import TYPE_CHECKING
56

67
from flask import (
78
Flask,
89
Request,
10+
abort,
911
jsonify,
1012
redirect,
1113
render_template,
14+
request,
1215
session,
1316
url_for,
1417
)
@@ -123,16 +126,40 @@ def process_invoices() -> str:
123126
@app.route("/process-invoices/run/<run_type>")
124127
@login_required
125128
def process_invoices_run(run_type: str) -> str | Response:
129+
if run_type == "review":
130+
return redirect(url_for("process_invoices_run_execute", run_type=run_type))
131+
elif run_type == "final": # noqa: RET505
132+
return redirect(url_for("process_invoices_confirm_final_run"))
133+
return abort(400, description=f"Invalid run type: '{run_type}'")
134+
135+
@app.route("/process-invoices/run/final/confirm")
136+
@login_required
137+
def process_invoices_confirm_final_run() -> str:
138+
return render_template("process_invoices_confirm_final_run.html")
139+
140+
@app.route("/process-invoices/run/<run_type>/execute")
141+
@login_required
142+
def process_invoices_run_execute(run_type: str) -> str | Response:
126143
ecs_client = ECSClient()
127144
if active_tasks := ecs_client.get_active_tasks():
128145
return render_template(
129146
"errors/error_400_cannot_run_multiple_tasks.html",
130147
active_tasks=active_tasks,
131148
)
149+
132150
if run_type == "review":
133151
task_arn = ecs_client.execute_review_run()
134152
elif run_type == "final":
135-
task_arn = ecs_client.execute_final_run()
153+
if request.referrer and request.referrer.endswith(
154+
"process-invoices/run/final/confirm"
155+
):
156+
task_arn = ecs_client.execute_final_run()
157+
else:
158+
return abort(
159+
400, description="Unable to confirm execution of 'final' run."
160+
)
161+
else:
162+
return abort(400, description=f"Invalid run type: '{run_type}'")
136163
task_id = task_arn.split("/")[-1] # type: ignore[union-attr]
137164
log_activity(f"executed a '{run_type}' run (task ID = '{task_id}').")
138165
return redirect(url_for("process_invoices_status", task_id=task_id))
@@ -157,12 +184,13 @@ def process_invoices_status(task_id: str) -> str:
157184
@app.route("/process-invoices/status/<task_id>/data")
158185
@login_required
159186
def process_invoices_status_data(task_id: str) -> Response:
187+
t_0 = time.time()
160188
try:
161189
task_status, logs = get_task_status_and_logs(task_id)
162190
except ECSTaskLogStreamDoesNotExistError:
163191
task_status = "UNKNOWN"
164192
logs = ["Log stream does not exist."]
165-
193+
logger.info(f"Data route elapsed: {time.time()-t_0}")
166194
return jsonify({"status": task_status, "logs": logs})
167195

168196
@app.route("/logout")

webapp/config.py

Lines changed: 17 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,27 @@
11
# ruff: noqa:ANN401, N802
22

3-
import json
43
import logging
54
import os
6-
from json import JSONDecodeError
75
from typing import Any
86

97
import sentry_sdk
108
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
119

12-
from webapp.exceptions import InvalidNetworkConfigurationError
13-
1410
logger = logging.getLogger("__name__")
1511

1612

1713
class Config:
1814
REQUIRED_ENV_VARS = (
15+
"ALMA_SAP_INVOICES_CLOUDWATCH_LOG_GROUP",
1916
"ALMA_SAP_INVOICES_ECR_IMAGE_NAME",
2017
"ALMA_SAP_INVOICES_ECS_CLUSTER",
18+
"ALMA_SAP_INVOICES_ECS_GROUPS",
19+
"ALMA_SAP_INVOICES_ECS_SUBNETS",
2120
"ALMA_SAP_INVOICES_ECS_TASK_DEFINITION",
22-
"ALMA_SAP_INVOICES_ECS_NETWORK_CONFIG",
23-
"ALMA_SAP_INVOICES_CLOUDWATCH_LOG_GROUP",
2421
"LOGIN_DISABLED",
2522
"SECRET_KEY",
2623
"WORKSPACE",
24+
"SENTRY_DSN",
2725
)
2826
OPTIONAL_ENV_VARS = ("AWS_DEFAULT_REGION",)
2927

@@ -34,62 +32,29 @@ def __getattr__(self, name: str) -> Any:
3432
message = f"'{name}' not a valid configuration variable"
3533
raise AttributeError(message)
3634

37-
def _getenv(self, name: str) -> Any:
38-
if value := os.getenv(name):
39-
return value
40-
if name in self.REQUIRED_ENV_VARS:
41-
message = f"'{name}' is a required environment variable."
42-
raise AttributeError(message)
43-
return None
44-
4535
@property
4636
def ALMA_SAP_INVOICES_ECS_NETWORK_CONFIG(self) -> dict:
47-
network_config = os.getenv("ALMA_SAP_INVOICES_ECS_NETWORK_CONFIG")
48-
try:
49-
return json.loads(network_config) # type: ignore[arg-type]
50-
except (TypeError, JSONDecodeError) as error:
51-
raise InvalidNetworkConfigurationError(error) from error
52-
53-
@property
54-
def ALMA_SAP_INVOICES_ECR_IMAGE_NAME(self) -> str:
55-
return self._getenv("ALMA_SAP_INVOICES_ECR_IMAGE_NAME")
56-
57-
@property
58-
def ALMA_SAP_INVOICES_ECS_CLUSTER(self) -> str:
59-
return self._getenv("ALMA_SAP_INVOICES_ECS_CLUSTER")
60-
61-
@property
62-
def ALMA_SAP_INVOICES_ECS_TASK_DEFINITION(self) -> str:
63-
return self._getenv("ALMA_SAP_INVOICES_ECS_TASK_DEFINITION")
64-
65-
@property
66-
def ALMA_SAP_INVOICES_CLOUDWATCH_LOG_GROUP(self) -> str:
67-
return self._getenv("ALMA_SAP_INVOICES_CLOUDWATCH_LOG_GROUP")
37+
security_groups = self.ALMA_SAP_INVOICES_ECS_GROUPS
38+
subnets = self.ALMA_SAP_INVOICES_ECS_SUBNETS
39+
return {
40+
"awsvpcConfiguration": {
41+
"subnets": subnets.split(","),
42+
"securityGroups": security_groups.split(","),
43+
"assignPublicIp": "DISABLED",
44+
}
45+
}
6846

6947
@property
7048
def AWS_DEFAULT_REGION(self) -> str:
71-
if region_name := self._getenv("AWS_DEFAULT_REGION"):
72-
return region_name
73-
return "us-east-1"
49+
return os.getenv("AWS_DEFAULT_REGION", "us-east-1")
7450

7551
@property
7652
def LOGIN_DISABLED(self) -> bool:
77-
if self._getenv("LOGIN_DISABLED").lower() == "true": # noqa: SIM103
78-
return True
53+
if login_disabled := os.getenv("LOGIN_DISABLED"): # noqa: SIM102
54+
if login_disabled.lower() == "true":
55+
return True
7956
return False
8057

81-
@property
82-
def SECRET_KEY(self) -> str:
83-
return self._getenv("SECRET_KEY")
84-
85-
@property
86-
def SENTRY_DSN(self) -> str:
87-
return self._getenv("SENTRY_DSN")
88-
89-
@property
90-
def WORKSPACE(self) -> str:
91-
return self._getenv("WORKSPACE")
92-
9358
def check_required_env_vars(self) -> None:
9459
"""Method to raise exception if required env vars not set."""
9560
missing_vars = [var for var in self.REQUIRED_ENV_VARS if not os.getenv(var)]

webapp/exceptions.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
1-
class InvalidNetworkConfigurationError(Exception):
2-
"""Exception to raise when ALMA_SAP_INVOICES_ECS_NETWORK_CONFIG is invalid."""
3-
4-
51
class ECSTaskLogStreamDoesNotExistError(Exception):
62
"""Exception to raise when a log stream is not found."""
73

0 commit comments

Comments
 (0)