Skip to content

Commit 79117b5

Browse files
committed
release/v2.7.5
1 parent 157fa6e commit 79117b5

File tree

7 files changed

+211
-30
lines changed

7 files changed

+211
-30
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [2.7.5] - 2025-01
8+
### Fixed
9+
- Updated aws-lambda-powertools from version 2.39.1 to 3.4.1
10+
- Improved handling of workspace termination logic
11+
712
## [2.7.4] - 2024-11
813
### Fixed
914
- Upgrade cross-spawn to mitigate [CVE-2024-21538](https://nvd.nist.gov/vuln/detail/CVE-2024-21538)

source/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

source/package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "cost-optimizer-for-amazon-workspaces",
3-
"version": "2.7.4",
3+
"version": "2.7.5",
44
"description": "Cost Optimizer for Amazon Workspaces (SO0018)",
55
"license": "Apache-2.0",
66
"repository": {
@@ -18,10 +18,10 @@
1818
"test": "jest --coverage",
1919
"license-report": "license-report --output=csv --delimiter=' under ' --fields=name --fields=licenseType",
2020
"cdk": "cdk",
21-
"bootstrap": "SOLUTION_VERSION=v2.7.4 SOLUTION_NAME=cost-optimizer-for-amazon-workspaces SOLUTION_TRADEMARKEDNAME=cost-optimizer-for-amazon-workspaces cdk bootstrap",
22-
"deploy": "SOLUTION_VERSION=v2.7.4 SOLUTION_NAME=cost-optimizer-for-amazon-workspaces SOLUTION_TRADEMARKEDNAME=cost-optimizer-for-amazon-workspaces cdk deploy cost-optimizer-for-amazon-workspaces",
23-
"deploySpoke": "SOLUTION_VERSION=v2.7.4 SOLUTION_NAME=cost-optimizer-for-amazon-workspaces SOLUTION_TRADEMARKEDNAME=cost-optimizer-for-amazon-workspaces cdk deploy cost-optimizer-for-amazon-workspaces-spoke",
24-
"synth": "SOLUTION_VERSION=v2.7.4 SOLUTION_NAME=cost-optimizer-for-amazon-workspaces SOLUTION_TRADEMARKEDNAME=cost-optimizer-for-amazon-workspaces DIST_OUTPUT_BUCKET=solutions-reference cdk synth"
21+
"bootstrap": "SOLUTION_VERSION=v2.7.5 SOLUTION_NAME=cost-optimizer-for-amazon-workspaces SOLUTION_TRADEMARKEDNAME=cost-optimizer-for-amazon-workspaces cdk bootstrap",
22+
"deploy": "SOLUTION_VERSION=v2.7.5 SOLUTION_NAME=cost-optimizer-for-amazon-workspaces SOLUTION_TRADEMARKEDNAME=cost-optimizer-for-amazon-workspaces cdk deploy cost-optimizer-for-amazon-workspaces",
23+
"deploySpoke": "SOLUTION_VERSION=v2.7.5 SOLUTION_NAME=cost-optimizer-for-amazon-workspaces SOLUTION_TRADEMARKEDNAME=cost-optimizer-for-amazon-workspaces cdk deploy cost-optimizer-for-amazon-workspaces-spoke",
24+
"synth": "SOLUTION_VERSION=v2.7.5 SOLUTION_NAME=cost-optimizer-for-amazon-workspaces SOLUTION_TRADEMARKEDNAME=cost-optimizer-for-amazon-workspaces DIST_OUTPUT_BUCKET=solutions-reference cdk synth"
2525
},
2626
"devDependencies": {
2727
"@aws-cdk/assert": "2.68.0",

source/poetry.lock

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

source/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "cost-optimizer-for-amazon-workspaces"
3-
version = "2.7.4"
3+
version = "2.7.5"
44
description = "Cost Optimizer for Amazon Workspaces (SO0018)"
55
authors = ["AWS Solutions"]
66
license = "Apache-2.0"
@@ -19,7 +19,7 @@ requests = "^2.31.0"
1919
s3transfer = "0.10.2"
2020
six = "1.16.0"
2121
urllib3 = "2.2.2"
22-
aws-lambda-powertools = "2.39.1"
22+
aws-lambda-powertools = "3.4.1"
2323

2424
[tool.poetry.group.dev.dependencies]
2525
pytest = "^7.2.2"

source/workspaces_app/workspaces_app/__tests__/test_workspaces_helper.py

Lines changed: 143 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -618,8 +618,11 @@ def test_check_if_workspace_needs_to_be_terminated_returns_dry_run_is_dry_run_tr
618618
"terminateUnusedWorkspaces": "Dry Run",
619619
}
620620
workspace_id = "123qwe123qwe"
621+
billable_time = 0
621622
workspace_helper = workspaces_helper.WorkspacesHelper(session, settings)
622-
result = workspace_helper.check_if_workspace_needs_to_be_terminated(workspace_id)
623+
result = workspace_helper.check_if_workspace_needs_to_be_terminated(
624+
workspace_id, billable_time
625+
)
623626
assert result == "Yes - Dry Run"
624627

625628

@@ -639,8 +642,11 @@ def test_check_if_workspace_needs_to_be_terminated_returns_dry_run_is_dry_run_fa
639642
"terminateUnusedWorkspaces": "Dry Run",
640643
}
641644
workspace_id = "123qwe123qwe"
645+
billable_time = 0
642646
workspace_helper = workspaces_helper.WorkspacesHelper(session, settings)
643-
result = workspace_helper.check_if_workspace_needs_to_be_terminated(workspace_id)
647+
result = workspace_helper.check_if_workspace_needs_to_be_terminated(
648+
workspace_id, billable_time
649+
)
644650
assert result == "Yes - Dry Run"
645651

646652

@@ -660,8 +666,11 @@ def test_check_if_workspace_needs_to_be_terminated_returns_empty_string_is_dry_r
660666
"terminateUnusedWorkspaces": "Yes",
661667
}
662668
workspace_id = "123qwe123qwe"
669+
billable_time = 0
663670
workspace_helper = workspaces_helper.WorkspacesHelper(session, settings)
664-
result = workspace_helper.check_if_workspace_needs_to_be_terminated(workspace_id)
671+
result = workspace_helper.check_if_workspace_needs_to_be_terminated(
672+
workspace_id, billable_time
673+
)
665674
assert result == ""
666675

667676

@@ -694,10 +703,13 @@ def test_check_if_workspace_needs_to_be_terminated_returns_yes_is_dry_run_false(
694703
},
695704
}
696705
workspace_id = "123qwe123qwe"
706+
billable_time = 0
697707
workspace_helper = workspaces_helper.WorkspacesHelper(session, settings)
698708
mocker.patch.object(workspace_helper, "terminate_unused_workspace")
699709
workspace_helper.terminate_unused_workspace.return_value = "Yes"
700-
result = workspace_helper.check_if_workspace_needs_to_be_terminated(workspace_id)
710+
result = workspace_helper.check_if_workspace_needs_to_be_terminated(
711+
workspace_id, billable_time
712+
)
701713
assert result == "Yes"
702714

703715

@@ -735,6 +747,31 @@ def test_check_if_workspace_used_for_selected_period_returns_true_if_timestamp_i
735747
assert result is True
736748

737749

750+
@freeze_time("2025-01-15 12:00:00")
751+
def test_check_if_workspace_needs_to_be_terminated_returns_empty_string_with_billable_time(
752+
session,
753+
):
754+
settings = {
755+
"region": "us-east-1",
756+
"hourlyLimits": 10,
757+
"testEndOfMonth": "yes",
758+
"isDryRun": True,
759+
"startTime": 1,
760+
"endTime": 2,
761+
"terminateUnusedWorkspaces": "Dry Run",
762+
"dateTimeValues": {
763+
"current_month_last_day": True,
764+
},
765+
}
766+
workspace_id = "123qwe123qwe"
767+
billable_time = 5
768+
workspace_helper = workspaces_helper.WorkspacesHelper(session, settings)
769+
result = workspace_helper.check_if_workspace_needs_to_be_terminated(
770+
workspace_id, billable_time
771+
)
772+
assert result == ""
773+
774+
738775
def test_get_last_known_user_connection_timestamp_returns_last_connected_time_value(
739776
session,
740777
):
@@ -769,6 +806,50 @@ def test_get_last_known_user_connection_timestamp_returns_last_connected_time_va
769806
client_stubber.deactivate()
770807

771808

809+
def test_get_last_known_user_connection_timestamp_handles_pagination(session):
810+
settings = {
811+
"region": "us-east-1",
812+
"hourlyLimits": 10,
813+
"testEndOfMonth": "yes",
814+
"isDryRun": True,
815+
"startTime": 1,
816+
"endTime": 2,
817+
}
818+
workspace_id = "ws-12345678"
819+
last_known_timestamp = datetime.datetime(
820+
2023, 1, 1, 12, 0, 0, tzinfo=datetime.timezone.utc
821+
)
822+
823+
workspace_helper = workspaces_helper.WorkspacesHelper(session, settings)
824+
client_stubber = Stubber(workspace_helper.workspaces_client)
825+
826+
response1 = {"WorkspacesConnectionStatus": [], "NextToken": "next-token"}
827+
expected_params1 = {"WorkspaceIds": [workspace_id]}
828+
829+
response2 = {
830+
"WorkspacesConnectionStatus": [
831+
{
832+
"WorkspaceId": workspace_id,
833+
"LastKnownUserConnectionTimestamp": last_known_timestamp,
834+
}
835+
]
836+
}
837+
expected_params2 = {"WorkspaceIds": [workspace_id], "NextToken": "next-token"}
838+
839+
client_stubber.add_response(
840+
"describe_workspaces_connection_status", response1, expected_params1
841+
)
842+
client_stubber.add_response(
843+
"describe_workspaces_connection_status", response2, expected_params2
844+
)
845+
client_stubber.activate()
846+
847+
result = workspace_helper.get_last_known_user_connection_timestamp(workspace_id)
848+
849+
assert result == last_known_timestamp
850+
client_stubber.deactivate()
851+
852+
772853
def test_get_last_known_user_connection_timestamp_returns_resource_unavailable_for_exception(
773854
session,
774855
):
@@ -1040,6 +1121,60 @@ def test_get_termination_status_returns_empty_string_when_workspace_not_availabl
10401121
assert result == ("", time.strftime("%Y-%m-%d", test_time.timetuple()))
10411122

10421123

1124+
def test_get_termination_status_with_billable_hours(mocker, session):
1125+
settings = {
1126+
"region": "us-east-1",
1127+
"hourlyLimits": 10,
1128+
"testEndOfMonth": True,
1129+
"isDryRun": True,
1130+
"startTime": 1,
1131+
"endTime": 2,
1132+
"TerminateUnusedWorkspaces": "Dry Run",
1133+
"dateTimeValues": {
1134+
"start_time_for_current_month": "",
1135+
"end_time_for_current_month": "",
1136+
"last_day_current_month": "",
1137+
"first_day_selected_month": "2025-01-01",
1138+
"start_time_selected_date": "2025-01-01T00:00:00Z",
1139+
"end_time_selected_date": "2025-01-02T00:00:00Z",
1140+
"current_month_last_day": True,
1141+
"date_today": "",
1142+
"date_for_s3_key": "",
1143+
},
1144+
}
1145+
workspace_helper = workspaces_helper.WorkspacesHelper(session, settings)
1146+
1147+
workspace_id = "ws-12345678"
1148+
billable_time = 10
1149+
tags = [{"Key": "TestTag", "Value": "TestValue"}]
1150+
1151+
mock_timestamp = datetime.datetime(2025, 1, 15, 12, 0, 0)
1152+
mocker.patch.object(
1153+
workspace_helper,
1154+
"get_last_known_user_connection_timestamp",
1155+
return_value=mock_timestamp,
1156+
)
1157+
mocker.patch.object(
1158+
workspace_utils, "is_terminate_workspace_enabled", return_value=True
1159+
)
1160+
mocker.patch.object(
1161+
workspace_helper, "check_if_workspace_available_on_first_day_selected_month"
1162+
)
1163+
mocker.patch.object(workspace_utils, "check_if_workspace_used_for_selected_period")
1164+
mocker.patch.object(workspace_helper, "check_if_workspace_needs_to_be_terminated")
1165+
1166+
result = workspace_helper.get_termination_status(workspace_id, billable_time, tags)
1167+
1168+
assert result == ("", "2025-01-15")
1169+
workspace_helper.get_last_known_user_connection_timestamp.assert_called_once_with(
1170+
workspace_id
1171+
)
1172+
workspace_utils.is_terminate_workspace_enabled.assert_called_once()
1173+
workspace_helper.check_if_workspace_available_on_first_day_selected_month.assert_called_once()
1174+
workspace_utils.check_if_workspace_used_for_selected_period.assert_called_once()
1175+
workspace_helper.check_if_workspace_needs_to_be_terminated.assert_not_called()
1176+
1177+
10431178
def test_get_workspaces_for_directory_use_next_token(session):
10441179
settings = {
10451180
"region": "us-east-1",
@@ -1486,8 +1621,11 @@ def test_check_if_workspace_needs_to_be_terminated_returns_empty_string_for_last
14861621
},
14871622
}
14881623
workspace_id = "123qwe123qwe"
1624+
billable_time = 0
14891625
workspace_helper = workspaces_helper.WorkspacesHelper(session, settings)
1490-
result = workspace_helper.check_if_workspace_needs_to_be_terminated(workspace_id)
1626+
result = workspace_helper.check_if_workspace_needs_to_be_terminated(
1627+
workspace_id, billable_time
1628+
)
14911629
assert result == ""
14921630

14931631

0 commit comments

Comments
 (0)