Skip to content

Commit e14d2b6

Browse files
authored
Add validation to block updating order of tags during cluster update (#7134)
* Add validation that does not allow for cluster update if tag order has been modified * Update CHANGELOG * Add update policy for tag order changes * Fix tox
1 parent 0c3cbf6 commit e14d2b6

File tree

6 files changed

+199
-3
lines changed

6 files changed

+199
-3
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ CHANGELOG
99
- Upgrade jmespath to ~=1.0 (from ~=0.10).
1010
- Upgrade tabulate to <=0.9.0 (from <=0.8.10).
1111

12+
**BUG FIXES**
13+
- Add validation to block updates that change tag order. Blocking such change prevents update failures.
14+
1215
3.14.1
1316
------
1417

cli/src/pcluster/config/config_patch.py

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from typing import List
1717

1818
from pcluster.config.update_policy import UpdatePolicy
19+
from pcluster.constants import ORDER_SENSITIVE_PARAMETERS
1920
from pcluster.schemas.cluster_schema import ClusterSchema
2021
from pcluster.schemas.common_schema import BaseSchema
2122

@@ -181,14 +182,48 @@ def _compare_list(self, base_section, target_section, param_path, data_key, fiel
181182
"""
182183
update_key = field_obj.metadata.get("update_key")
183184

185+
# Get the lists
186+
base_list = base_section.get(data_key, []) if base_section else []
187+
target_list = target_section.get(data_key, []) if target_section else []
188+
189+
# For Tags, check if this is ONLY an order change (same content, different order)
190+
if data_key in ORDER_SENSITIVE_PARAMETERS and base_list != target_list:
191+
# Check if it's an order-only change
192+
try:
193+
# We can assume there are no duplicates due to a validator
194+
base_set = {frozenset(item.items()) for item in base_list}
195+
target_set = {frozenset(item.items()) for item in target_list}
196+
is_order_only_change = base_set == target_set
197+
except (TypeError, AttributeError):
198+
LOGGER.warning("Unable to compare order sensitive parameters.")
199+
is_order_only_change = False
200+
201+
# If it's ONLY an order change, block it here and return
202+
if is_order_only_change:
203+
self.changes.append(
204+
Change(
205+
param_path,
206+
data_key,
207+
base_list,
208+
target_list,
209+
UpdatePolicy.UNSUPPORTED_ORDER_CHANGE,
210+
is_list=True,
211+
)
212+
)
213+
return # Don't process individual items
214+
# Otherwise, fall through to item-by-item comparison below
215+
# The reason I check if it is an order only change first is because if I just do I order dependent
216+
# comparison and fail if there is a difference, then we don't get the item by item comparison in the
217+
# change set if the modification was not the order.
218+
184219
# Compare items in the list by searching the right item to compare through update_key value
185220
# First, compare all sections from target vs base config and mark visited base sections.
186-
for target_nested_section in target_section.get(data_key, []):
221+
for target_nested_section in target_list:
187222
update_key_value = target_nested_section.get(update_key)
188223
base_nested_section = next(
189224
(
190225
nested_section
191-
for nested_section in base_section.get(data_key, [])
226+
for nested_section in base_list
192227
if nested_section.get(update_key) == update_key_value
193228
),
194229
None,

cli/src/pcluster/config/update_policy.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -682,6 +682,18 @@ def get_pool_name_from_change_paths(change):
682682
+ ". If you need this change, please consider creating a new cluster instead of updating the existing one.",
683683
)
684684

685+
UpdatePolicy.UNSUPPORTED_ORDER_CHANGE = UpdatePolicy(
686+
name="UNSUPPORTED_ORDER_CHANGE",
687+
level=1000, # Same level as UNSUPPORTED since it should also block updates
688+
fail_reason=lambda change, patch: (
689+
f"Update actions are not currently supported for the '{change.key}' parameter. "
690+
f"The order of {change.key} has changed, but order changes are not supported"
691+
),
692+
action_needed=lambda change, patch: (
693+
f"Restore the original order of '{change.key}' to match the current configuration."
694+
),
695+
)
696+
685697
# Block update if cluster has a managed Fsx for Lustre FileSystem, otherwise fallback to QueueUpdateStrategy
686698
UpdatePolicy.MANAGED_FSX = UpdatePolicy(
687699
name="MANAGED_FSX",

cli/src/pcluster/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,3 +354,5 @@ class Operation(Enum):
354354
}
355355
# Capacity Block states that are considered inactive (cannot check health status)
356356
CAPACITY_BLOCK_INACTIVE_STATES = ["scheduled", "payment-pending", "assessing", "delayed"]
357+
358+
ORDER_SENSITIVE_PARAMETERS = ["Tags"]

cli/tests/pcluster/config/test_config_patch.py

Lines changed: 117 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,12 @@ def _compare_change(source, target):
8686
def _sorting_func(change):
8787
if change.is_list:
8888
if isinstance(change.new_value, dict):
89-
return change.new_value["Name"]
89+
if "Key" in change.new_value:
90+
return change.new_value["Key"]
91+
elif "Name" in change.new_value:
92+
return change.new_value["Name"]
93+
else:
94+
return "-"
9095
else:
9196
return "-"
9297
else:
@@ -1133,3 +1138,114 @@ def test_patch_check_cluster_resource_bucket(
11331138
line = ["{0}".format(element) if isinstance(element, str) else element for element in line]
11341139
assert_that(expected_message_rows).contains(line)
11351140
assert_that(patch_allowed).is_equal_to(not expected_error_row)
1141+
1142+
1143+
@pytest.mark.parametrize(
1144+
"base_tags, target_tags, expected_changes, expected_policy",
1145+
[
1146+
pytest.param(
1147+
[{"Key": "test1", "Value": "val1"}, {"Key": "test2", "Value": "val2"}],
1148+
[{"Key": "test2", "Value": "val2"}, {"Key": "test1", "Value": "val1"}],
1149+
[
1150+
Change(
1151+
[],
1152+
"Tags",
1153+
[{"Key": "test1", "Value": "val1"}, {"Key": "test2", "Value": "val2"}],
1154+
[{"Key": "test2", "Value": "val2"}, {"Key": "test1", "Value": "val1"}],
1155+
UpdatePolicy.UNSUPPORTED_ORDER_CHANGE,
1156+
is_list=True,
1157+
)
1158+
],
1159+
UpdatePolicy.UNSUPPORTED_ORDER_CHANGE,
1160+
id="order_only_change",
1161+
),
1162+
pytest.param(
1163+
[{"Key": "test1", "Value": "val1"}, {"Key": "test2", "Value": "val2"}],
1164+
[{"Key": "test1", "Value": "val1"}, {"Key": "test2", "Value": "val2"}, {"Key": "test3", "Value": "val3"}],
1165+
[
1166+
Change(
1167+
[],
1168+
"Tags",
1169+
None,
1170+
{"Key": "test3", "Value": "val3"},
1171+
UpdatePolicy.UNSUPPORTED,
1172+
is_list=True,
1173+
)
1174+
],
1175+
UpdatePolicy.UNSUPPORTED,
1176+
id="tag_addition",
1177+
),
1178+
pytest.param(
1179+
[{"Key": "test1", "Value": "val1"}, {"Key": "test2", "Value": "val2"}],
1180+
[{"Key": "test1", "Value": "val1"}],
1181+
[
1182+
Change(
1183+
[],
1184+
"Tags",
1185+
{"Key": "test2", "Value": "val2"},
1186+
None,
1187+
UpdatePolicy.UNSUPPORTED,
1188+
is_list=True,
1189+
)
1190+
],
1191+
UpdatePolicy.UNSUPPORTED,
1192+
id="tag_removal",
1193+
),
1194+
pytest.param(
1195+
[{"Key": "test1", "Value": "old_value"}],
1196+
[{"Key": "test1", "Value": "new_value"}],
1197+
[
1198+
Change(
1199+
["Tags[test1]"],
1200+
"Value",
1201+
"old_value",
1202+
"new_value",
1203+
UpdatePolicy.UNSUPPORTED,
1204+
is_list=False,
1205+
)
1206+
],
1207+
UpdatePolicy.UNSUPPORTED,
1208+
id="tag_value_modification",
1209+
),
1210+
pytest.param(
1211+
[{"Key": "test1", "Value": "val1"}, {"Key": "test2", "Value": "val2"}],
1212+
[{"Key": "test3", "Value": "val3"}, {"Key": "test2", "Value": "val2"}, {"Key": "test1", "Value": "val1"}],
1213+
[
1214+
Change(
1215+
[],
1216+
"Tags",
1217+
None,
1218+
{"Key": "test3", "Value": "val3"},
1219+
UpdatePolicy.UNSUPPORTED,
1220+
is_list=True,
1221+
)
1222+
],
1223+
UpdatePolicy.UNSUPPORTED,
1224+
id="order_change_plus_addition",
1225+
),
1226+
pytest.param(
1227+
[{"Key": "test1", "Value": "val1"}, {"Key": "test2", "Value": "val2"}],
1228+
[{"Key": "test1", "Value": "val1"}, {"Key": "test2", "Value": "val2"}],
1229+
[],
1230+
UpdatePolicy.SUPPORTED,
1231+
id="no_change_identical_tags",
1232+
),
1233+
],
1234+
)
1235+
def test_tag_updates(
1236+
mocker, test_datadir, pcluster_config_reader, base_tags, target_tags, expected_changes, expected_policy
1237+
):
1238+
"""Test various tag update scenarios including order changes, additions, and modifications."""
1239+
mock_aws_api(mocker)
1240+
dst_config_file = "pcluster.config.dst.yaml"
1241+
_duplicate_config_file(dst_config_file, test_datadir)
1242+
1243+
src_dict = {"tags": base_tags}
1244+
src_config_file = pcluster_config_reader(**src_dict)
1245+
src_conf = _load_config(src_config_file)
1246+
1247+
dst_dict = {"tags": target_tags}
1248+
dst_config_file = pcluster_config_reader(dst_config_file, **dst_dict)
1249+
dst_conf = _load_config(dst_config_file)
1250+
1251+
_check_patch(src_conf.source_config, dst_conf.source_config, expected_changes, expected_policy)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
Image:
2+
Os: alinux2
3+
HeadNode:
4+
InstanceType: t3.micro
5+
Networking:
6+
SubnetId: subnet-12345678
7+
Ssh:
8+
KeyName: test-key
9+
Scheduling:
10+
Scheduler: slurm
11+
SlurmQueues:
12+
- Name: queue1
13+
Networking:
14+
SubnetIds:
15+
- subnet-12345678
16+
ComputeResources:
17+
- Name: compute-resource1
18+
InstanceType: t3.micro
19+
MinCount: 1
20+
MaxCount: 10
21+
{% if tags %}
22+
Tags:
23+
{% for tag in tags %}
24+
- Key: {{ tag.Key }}
25+
Value: {{ tag.Value }}
26+
{% endfor %}
27+
{% endif %}
28+

0 commit comments

Comments
 (0)