diff --git a/src/sagemaker/hyperpod/cluster_management/hp_cluster_stack.py b/src/sagemaker/hyperpod/cluster_management/hp_cluster_stack.py index d888e9e7..d8ceb7de 100644 --- a/src/sagemaker/hyperpod/cluster_management/hp_cluster_stack.py +++ b/src/sagemaker/hyperpod/cluster_management/hp_cluster_stack.py @@ -399,14 +399,21 @@ def list(region: Optional[str] = None, stack_status_filter: Optional[List[str]] response = cf.list_stacks(**list_params) + # Paginate through all results + all_summaries = response.get('StackSummaries', []) + while 'NextToken' in response: + list_params['NextToken'] = response['NextToken'] + response = cf.list_stacks(**list_params) + all_summaries.extend(response.get('StackSummaries', [])) + # Only filter DELETE_COMPLETE when no explicit filter is provided - if stack_status_filter is None and 'StackSummaries' in response: - response['StackSummaries'] = [ - stack for stack in response['StackSummaries'] + if stack_status_filter is None: + all_summaries = [ + stack for stack in all_summaries if stack.get('StackStatus') != 'DELETE_COMPLETE' ] - return response + return {'StackSummaries': all_summaries} except cf.exceptions.ClientError as e: error_code = e.response['Error']['Code'] diff --git a/test/unit_tests/cluster_management/test_hp_cluster_stack.py b/test/unit_tests/cluster_management/test_hp_cluster_stack.py index 9acc75b4..55d39e9a 100644 --- a/test/unit_tests/cluster_management/test_hp_cluster_stack.py +++ b/test/unit_tests/cluster_management/test_hp_cluster_stack.py @@ -511,6 +511,28 @@ def test_list_default_filters_delete_complete(self, mock_create_client): assert 'updating-stack' in stack_names assert 'deleted-stack' not in stack_names + @patch('sagemaker.hyperpod.cluster_management.hp_cluster_stack.create_boto3_client') + def test_list_paginates_through_all_pages(self, mock_create_client): + """Test that list() follows NextToken to collect all pages.""" + mock_cf_client = MagicMock() + mock_create_client.return_value = mock_cf_client + + page1 = { + 'StackSummaries': [{'StackName': 'stack-1', 'StackStatus': 'CREATE_COMPLETE'}], + 'NextToken': 'token-1', + } + page2 = { + 'StackSummaries': [{'StackName': 'stack-2', 'StackStatus': 'CREATE_COMPLETE'}], + } + mock_cf_client.list_stacks.side_effect = [page1, page2] + + result = HpClusterStack.list() + + assert mock_cf_client.list_stacks.call_count == 2 + assert len(result['StackSummaries']) == 2 + stack_names = [s['StackName'] for s in result['StackSummaries']] + assert stack_names == ['stack-1', 'stack-2'] + @patch('sagemaker.hyperpod.cluster_management.hp_cluster_stack.create_boto3_client') def test_list_with_status_filter(self, mock_create_client): """Test that list() uses API filter and returns only matching stacks.""" @@ -553,7 +575,7 @@ def test_list_empty_response(self, mock_create_client): result = HpClusterStack.list() # Assert - assert result == {} + assert result == {'StackSummaries': []} @patch('sagemaker.hyperpod.cluster_management.hp_cluster_stack.create_boto3_client') def test_list_with_region(self, mock_create_client):