Skip to content

Commit b04a2fd

Browse files
committed
Merge branch 'master' into is1639/nih-portal-api
2 parents 622f24f + ce96a9b commit b04a2fd

File tree

337 files changed

+14231
-4112
lines changed

Some content is hidden

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

337 files changed

+14231
-4112
lines changed

.env-devel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ DYNAMIC_SIDECAR_PROMETHEUS_MONITORING_NETWORKS=[]
129129
DYNAMIC_SIDECAR_PROMETHEUS_SERVICE_LABELS={}
130130
DYNAMIC_SIDECAR_API_SAVE_RESTORE_STATE_TIMEOUT=01:00:00
131131
DIRECTOR_V2_TRACING={}
132+
DIRECTOR_V2_DYNAMIC_SCHEDULER_ENABLED=1
132133

133134
# DYNAMIC_SCHEDULER ----
134135
DYNAMIC_SCHEDULER_LOGLEVEL=INFO
@@ -141,6 +142,7 @@ DYNAMIC_SCHEDULER_UI_STORAGE_SECRET=adminadmin
141142
FUNCTION_SERVICES_AUTHORS='{"UN": {"name": "Unknown", "email": "[email protected]", "affiliation": "unknown"}}'
142143

143144
WEBSERVER_LICENSES={}
145+
WEBSERVER_FOGBUGZ={}
144146
LICENSES_ITIS_VIP_SYNCER_ENABLED=false
145147
LICENSES_ITIS_VIP_SYNCER_PERIODICITY=1D00:00:00
146148
LICENSES_ITIS_VIP_API_URL=https://replace-with-itis-api/{category}

.github/prompts/update-user-messages.prompt.md

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
mode: 'edit'
33
description: 'Update user messages'
4+
model: Claude Sonnet 3.5
45
---
56

67
This prompt guide is for updating user-facing messages in ${file} or ${selection}
@@ -43,7 +44,17 @@ When modifying user messages, follow **as close as possible** these rules:
4344
user_message("Unable to load project.", _version=1)
4445
```
4546

46-
3. **Message Style**: Follow **strictly** the guidelines in `${workspaceFolder}/docs/user-messages-guidelines.md`
47+
3. **Message Style**: Follow **STRICTLY ALL 10 GUIDELINES** in `${workspaceFolder}/docs/user-messages-guidelines.md`:
48+
- Be Clear and Concise
49+
- Provide Specific and Actionable Information
50+
- Avoid Technical Jargon
51+
- Use a Polite and Non-Blaming Tone
52+
- Avoid Negative Words and Phrases
53+
- Place Messages Appropriately
54+
- Use Inline Validation When Possible
55+
- Avoid Using All-Caps and Excessive Punctuation
56+
- **Use Humor Sparingly** - Avoid casual phrases like "Oops!", "Whoops!", or overly informal language
57+
- Offer Alternative Solutions or Support
4758

4859
4. **Preserve Context**: Ensure the modified message conveys the same meaning and context as the original.
4960

@@ -56,8 +67,10 @@ When modifying user messages, follow **as close as possible** these rules:
5667
# After
5768
user_message("Your session has expired. Please log in again.", _version=3)
5869
```
70+
5971
6. **Replace 'Study' by 'Project'**: If the message contains the word 'Study', replace it with 'Project' to align with our terminology.
6072

73+
7. **Professional Tone**: Maintain a professional, helpful tone. Avoid humor, casual expressions, or overly informal language that might not be appropriate for all users or situations.
6174

6275
## Examples
6376

@@ -91,4 +104,14 @@ return HttpErrorInfo(status.HTTP_404_NOT_FOUND, user_message("User not found.",
91104
return HttpErrorInfo(status.HTTP_404_NOT_FOUND, user_message("The requested user could not be found.", _version=2))
92105
```
93106

94-
Remember: The goal is to improve clarity and helpfulness for end-users while maintaining accurate versioning for tracking changes.
107+
### Example 4: Removing Humor (Guideline 9)
108+
109+
```python
110+
# Before
111+
user_message("Oops! Something went wrong, but we've noted it down and we'll sort it out ASAP. Thanks for your patience!")
112+
113+
# After
114+
user_message("Something went wrong on our end. We've been notified and will resolve this issue as soon as possible. Thank you for your patience.", _version=1)
115+
```
116+
117+
Remember: The goal is to improve clarity and helpfulness for end-users while maintaining accurate versioning for tracking changes. **Always check that your updated messages comply with ALL 10 guidelines, especially avoiding humor and maintaining a professional tone.**

.github/workflows/ci-testing-deploy.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1057,7 +1057,7 @@ jobs:
10571057
unit-test-dynamic-sidecar:
10581058
needs: changes
10591059
if: ${{ needs.changes.outputs.dynamic-sidecar == 'true' || github.event_name == 'push' || github.event.inputs.force_all_builds == 'true' }}
1060-
timeout-minutes: 18 # if this timeout gets too small, then split the tests
1060+
timeout-minutes: 19 # if this timeout gets too small, then split the tests
10611061
name: "[unit] dynamic-sidecar"
10621062
runs-on: ${{ matrix.os }}
10631063
strategy:

packages/aws-library/src/aws_library/ec2/__init__.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
from ._client import SimcoreEC2API
2-
from ._errors import EC2AccessError, EC2NotConnectedError, EC2RuntimeError
2+
from ._errors import (
3+
EC2AccessError,
4+
EC2InsufficientCapacityError,
5+
EC2NotConnectedError,
6+
EC2RuntimeError,
7+
)
38
from ._models import (
49
AWS_TAG_KEY_MAX_LENGTH,
510
AWS_TAG_KEY_MIN_LENGTH,
@@ -16,22 +21,22 @@
1621
)
1722

1823
__all__: tuple[str, ...] = (
19-
"AWSTagKey",
20-
"AWSTagValue",
21-
"AWS_TAG_KEY_MIN_LENGTH",
2224
"AWS_TAG_KEY_MAX_LENGTH",
23-
"AWS_TAG_VALUE_MIN_LENGTH",
25+
"AWS_TAG_KEY_MIN_LENGTH",
2426
"AWS_TAG_VALUE_MAX_LENGTH",
27+
"AWS_TAG_VALUE_MIN_LENGTH",
28+
"AWSTagKey",
29+
"AWSTagValue",
2530
"EC2AccessError",
2631
"EC2InstanceBootSpecific",
2732
"EC2InstanceConfig",
2833
"EC2InstanceData",
2934
"EC2InstanceType",
35+
"EC2InsufficientCapacityError",
3036
"EC2NotConnectedError",
3137
"EC2RuntimeError",
3238
"EC2Tags",
3339
"Resources",
3440
"SimcoreEC2API",
3541
)
36-
3742
# nopycln: file

packages/aws-library/src/aws_library/ec2/_client.py

Lines changed: 131 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,17 @@
1313
from settings_library.ec2 import EC2Settings
1414
from types_aiobotocore_ec2 import EC2Client
1515
from types_aiobotocore_ec2.literals import InstanceStateNameType, InstanceTypeType
16-
from types_aiobotocore_ec2.type_defs import FilterTypeDef, TagTypeDef
16+
from types_aiobotocore_ec2.type_defs import (
17+
FilterTypeDef,
18+
TagTypeDef,
19+
)
1720

1821
from ._error_handler import ec2_exception_handler
19-
from ._errors import EC2InstanceNotFoundError, EC2TooManyInstancesError
22+
from ._errors import (
23+
EC2InstanceNotFoundError,
24+
EC2InsufficientCapacityError,
25+
EC2SubnetsNotEnoughIPsError,
26+
)
2027
from ._models import (
2128
AWSTagKey,
2229
EC2InstanceConfig,
@@ -25,7 +32,13 @@
2532
EC2Tags,
2633
Resources,
2734
)
28-
from ._utils import compose_user_data, ec2_instance_data_from_aws_instance
35+
from ._utils import (
36+
check_max_number_of_instances_not_exceeded,
37+
compose_user_data,
38+
ec2_instance_data_from_aws_instance,
39+
get_subnet_azs,
40+
get_subnet_capacity,
41+
)
2942

3043
_logger = logging.getLogger(__name__)
3144

@@ -92,6 +105,11 @@ async def get_ec2_instance_capabilities(
92105
list_instances: list[EC2InstanceType] = []
93106
for instance in instance_types.get("InstanceTypes", []):
94107
with contextlib.suppress(KeyError):
108+
assert "InstanceType" in instance # nosec
109+
assert "VCpuInfo" in instance # nosec
110+
assert "DefaultVCpus" in instance["VCpuInfo"] # nosec
111+
assert "MemoryInfo" in instance # nosec
112+
assert "SizeInMiB" in instance["MemoryInfo"] # nosec
95113
list_instances.append(
96114
EC2InstanceType(
97115
name=instance["InstanceType"],
@@ -118,94 +136,145 @@ async def launch_instances(
118136
119137
Arguments:
120138
instance_config -- The EC2 instance configuration
121-
min_number_of_instances -- the minimal number of instances needed (fails if this amount cannot be reached)
139+
min_number_of_instances -- the minimal number of instances required (fails if this amount cannot be reached)
122140
number_of_instances -- the ideal number of instances needed (it it cannot be reached AWS will return a number >=min_number_of_instances)
123-
124-
Keyword Arguments:
125-
max_total_number_of_instances -- The total maximum allowed number of instances for this given instance_config (default: {10})
141+
max_total_number_of_instances -- The total maximum allowed number of instances for this given instance_config
126142
127143
Raises:
128-
EC2TooManyInstancesError:
144+
EC2TooManyInstancesError: max_total_number_of_instances would be exceeded
145+
EC2SubnetsNotEnoughIPsError: not enough IPs in the subnets
146+
EC2InsufficientCapacityError: not enough capacity in the subnets
147+
129148
130149
Returns:
131150
The created instance data infos
132151
"""
152+
133153
with log_context(
134154
_logger,
135155
logging.INFO,
136-
msg=f"launch {number_of_instances} AWS instance(s) {instance_config.type.name} with {instance_config.tags=}",
156+
msg=f"launch {number_of_instances} AWS instance(s) {instance_config.type.name}"
157+
f" with {instance_config.tags=} in {instance_config.subnet_ids=}",
137158
):
138159
# first check the max amount is not already reached
139-
current_instances = await self.get_instances(
140-
key_names=[instance_config.key_name], tags=instance_config.tags
160+
await check_max_number_of_instances_not_exceeded(
161+
self,
162+
instance_config,
163+
required_number_instances=number_of_instances,
164+
max_total_number_of_instances=max_total_number_of_instances,
141165
)
142-
if (
143-
len(current_instances) + number_of_instances
144-
> max_total_number_of_instances
145-
):
146-
raise EC2TooManyInstancesError(
147-
num_instances=max_total_number_of_instances
166+
167+
# NOTE: checking subnets capacity is not strictly needed as AWS will do it for us
168+
# but it gives us a chance to give early feedback to the user
169+
# and avoid trying to launch instances in subnets that are already full
170+
# and also allows to circumvent a moto bug that does not raise
171+
# InsufficientInstanceCapacity when a subnet is full
172+
subnet_id_to_available_ips = await get_subnet_capacity(
173+
self.client, subnet_ids=instance_config.subnet_ids
174+
)
175+
176+
total_available_ips = sum(subnet_id_to_available_ips.values())
177+
if total_available_ips < min_number_of_instances:
178+
raise EC2SubnetsNotEnoughIPsError(
179+
subnet_ids=instance_config.subnet_ids,
180+
instance_type=instance_config.type.name,
181+
available_ips=total_available_ips,
148182
)
149183

184+
# now let's not try to run instances in subnets that have not enough IPs
185+
subnet_ids_with_capacity = [
186+
subnet_id
187+
for subnet_id, capacity in subnet_id_to_available_ips.items()
188+
if capacity >= min_number_of_instances
189+
]
190+
150191
resource_tags: list[TagTypeDef] = [
151192
{"Key": tag_key, "Value": tag_value}
152193
for tag_key, tag_value in instance_config.tags.items()
153194
]
154195

155-
instances = await self.client.run_instances(
156-
ImageId=instance_config.ami_id,
157-
MinCount=min_number_of_instances,
158-
MaxCount=number_of_instances,
159-
IamInstanceProfile=(
160-
{"Arn": instance_config.iam_instance_profile}
161-
if instance_config.iam_instance_profile
162-
else {}
163-
),
164-
InstanceType=instance_config.type.name,
165-
InstanceInitiatedShutdownBehavior="terminate",
166-
KeyName=instance_config.key_name,
167-
TagSpecifications=[
168-
{"ResourceType": "instance", "Tags": resource_tags},
169-
{"ResourceType": "volume", "Tags": resource_tags},
170-
{"ResourceType": "network-interface", "Tags": resource_tags},
171-
],
172-
UserData=compose_user_data(instance_config.startup_script),
173-
NetworkInterfaces=[
174-
{
175-
"AssociatePublicIpAddress": True,
176-
"DeviceIndex": 0,
177-
"SubnetId": instance_config.subnet_id,
178-
"Groups": instance_config.security_group_ids,
179-
}
180-
],
181-
)
182-
instance_ids = [i["InstanceId"] for i in instances["Instances"]]
183-
_logger.info(
184-
"%s New instances launched: %s, waiting for them to start now...",
185-
len(instance_ids),
186-
instance_ids,
187-
)
196+
# Try each subnet in order until one succeeds
197+
for subnet_id in subnet_ids_with_capacity:
198+
try:
199+
_logger.debug(
200+
"Attempting to launch instances in subnet %s", subnet_id
201+
)
188202

189-
# wait for the instance to be in a pending state
190-
# NOTE: reference to EC2 states https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-lifecycle.html
191-
waiter = self.client.get_waiter("instance_exists")
192-
await waiter.wait(InstanceIds=instance_ids)
193-
_logger.debug("instances %s exists now.", instance_ids)
203+
instances = await self.client.run_instances(
204+
ImageId=instance_config.ami_id,
205+
MinCount=min_number_of_instances,
206+
MaxCount=number_of_instances,
207+
IamInstanceProfile=(
208+
{"Arn": instance_config.iam_instance_profile}
209+
if instance_config.iam_instance_profile
210+
else {}
211+
),
212+
InstanceType=instance_config.type.name,
213+
InstanceInitiatedShutdownBehavior="terminate",
214+
KeyName=instance_config.key_name,
215+
TagSpecifications=[
216+
{"ResourceType": "instance", "Tags": resource_tags},
217+
{"ResourceType": "volume", "Tags": resource_tags},
218+
{
219+
"ResourceType": "network-interface",
220+
"Tags": resource_tags,
221+
},
222+
],
223+
UserData=compose_user_data(instance_config.startup_script),
224+
NetworkInterfaces=[
225+
{
226+
"AssociatePublicIpAddress": True,
227+
"DeviceIndex": 0,
228+
"SubnetId": subnet_id,
229+
"Groups": instance_config.security_group_ids,
230+
}
231+
],
232+
)
233+
# If we get here, the launch succeeded
234+
break
235+
except botocore.exceptions.ClientError as exc:
236+
error_code = exc.response.get("Error", {}).get("Code")
237+
if error_code == "InsufficientInstanceCapacity":
238+
_logger.warning(
239+
"Insufficient capacity in subnet %s for instance type %s, trying next subnet",
240+
subnet_id,
241+
instance_config.type.name,
242+
)
243+
continue
244+
# For any other ClientError, re-raise to let the decorator handle it
245+
raise
246+
247+
else:
248+
subnet_zones = await get_subnet_azs(
249+
self.client, subnet_ids=subnet_ids_with_capacity
250+
)
251+
raise EC2InsufficientCapacityError(
252+
availability_zones=subnet_zones,
253+
instance_type=instance_config.type.name,
254+
)
255+
instance_ids = [
256+
i["InstanceId"] # pyright: ignore[reportTypedDictNotRequiredAccess]
257+
for i in instances["Instances"]
258+
]
259+
with log_context(
260+
_logger,
261+
logging.INFO,
262+
msg=f"{len(instance_ids)} instances: {instance_ids=} launched. Wait to reach pending state",
263+
):
264+
# wait for the instance to be in a pending state
265+
# NOTE: reference to EC2 states https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-lifecycle.html
266+
waiter = self.client.get_waiter("instance_exists")
267+
await waiter.wait(InstanceIds=instance_ids)
194268

195-
# NOTE: waiting for pending ensure we get all the IPs back
269+
# NOTE: waiting for pending ensures we get all the IPs back
196270
described_instances = await self.client.describe_instances(
197271
InstanceIds=instance_ids
198272
)
199273
assert "Instances" in described_instances["Reservations"][0] # nosec
200-
instance_datas = [
274+
return [
201275
await ec2_instance_data_from_aws_instance(self, i)
202276
for i in described_instances["Reservations"][0]["Instances"]
203277
]
204-
_logger.info(
205-
"%s are pending now",
206-
f"{instance_ids=}",
207-
)
208-
return instance_datas
209278

210279
@ec2_exception_handler(_logger)
211280
async def get_instances(

0 commit comments

Comments
 (0)