1+ # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+ #
3+ # Licensed under the Apache License, Version 2.0 (the "License"). You
4+ # may not use this file except in compliance with the License. A copy of
5+ # the License is located at
6+ #
7+ # http://aws.amazon.com/apache2.0/
8+ #
9+ # or in the "license" file accompanying this file. This file is
10+ # distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
11+ # ANY KIND, either express or implied. See the License for the specific
12+ # language governing permissions and limitations under the License.
13+ import unittest
14+ from unittest .mock import Mock , patch
15+ from click .testing import CliRunner
16+ from botocore .exceptions import ClientError
17+ from sagemaker .hyperpod .cli .commands .cluster import describe_cluster
18+
19+
20+ class DescribeClusterTest (unittest .TestCase ):
21+ def setUp (self ):
22+ self .runner = CliRunner ()
23+
24+ @patch ('sagemaker.hyperpod.cli.commands.cluster.get_sagemaker_client' )
25+ @patch ('sagemaker.hyperpod.cli.commands.cluster.boto3.Session' )
26+ @patch ('sagemaker.hyperpod.cli.commands.cluster.setup_logger' )
27+ def test_describe_cluster_happy_case (self , mock_setup_logger , mock_session , mock_get_sagemaker_client ):
28+ """Test successful cluster description with valid cluster name."""
29+ # Arrange
30+ mock_logger = Mock ()
31+ mock_setup_logger .return_value = mock_logger
32+
33+ mock_session_instance = Mock ()
34+ mock_session .return_value = mock_session_instance
35+
36+ mock_sm_client = Mock ()
37+ mock_get_sagemaker_client .return_value = mock_sm_client
38+
39+ # Mock successful cluster response
40+ cluster_response = {
41+ "ClusterArn" : "arn:aws:sagemaker:us-east-2:123456789012:cluster/test-cluster" ,
42+ "ClusterName" : "test-cluster" ,
43+ "ClusterStatus" : "InService" ,
44+ "CreationTime" : "2023-09-23T14:35:38.223000+00:00" ,
45+ "InstanceGroups" : [
46+ {
47+ "InstanceGroupName" : "controller-group" ,
48+ "InstanceType" : "ml.t3.medium" ,
49+ "CurrentCount" : 1 ,
50+ "TargetCount" : 1
51+ }
52+ ],
53+ "VpcConfig" : {
54+ "SecurityGroupIds" : ["sg-1234567890abcdef0" ],
55+ "Subnets" : ["subnet-1234567890abcdef0" ]
56+ },
57+ "Orchestrator" : {
58+ "Eks" : {
59+ "ClusterArn" : "arn:aws:eks:us-east-2:123456789012:cluster/eks-cluster"
60+ }
61+ }
62+ }
63+
64+ mock_sm_client .describe_cluster .return_value = cluster_response
65+
66+ # Act
67+ result = self .runner .invoke (describe_cluster , ["test-cluster" ])
68+
69+ # Assert
70+ assert result .exit_code == 0
71+ mock_sm_client .describe_cluster .assert_called_once_with (ClusterName = "test-cluster" )
72+ assert "📋 Cluster Details for: test-cluster" in result .output
73+ assert "test-cluster" in result .output
74+ assert "InService" in result .output
75+
76+ @patch ('sagemaker.hyperpod.cli.commands.cluster.get_sagemaker_client' )
77+ @patch ('sagemaker.hyperpod.cli.commands.cluster.boto3.Session' )
78+ @patch ('sagemaker.hyperpod.cli.commands.cluster.setup_logger' )
79+ def test_describe_cluster_with_region_flag (self , mock_setup_logger , mock_session , mock_get_sagemaker_client ):
80+ """Test cluster description with region flag specified."""
81+ # Arrange
82+ mock_logger = Mock ()
83+ mock_setup_logger .return_value = mock_logger
84+
85+ mock_session_instance = Mock ()
86+ mock_session .return_value = mock_session_instance
87+
88+ mock_sm_client = Mock ()
89+ mock_get_sagemaker_client .return_value = mock_sm_client
90+
91+ # Mock successful cluster response
92+ cluster_response = {
93+ "ClusterArn" : "arn:aws:sagemaker:us-west-2:123456789012:cluster/test-cluster" ,
94+ "ClusterName" : "test-cluster" ,
95+ "ClusterStatus" : "InService" ,
96+ "CreationTime" : "2023-09-23T14:35:38.223000+00:00" ,
97+ "InstanceGroups" : [
98+ {
99+ "InstanceGroupName" : "worker-group" ,
100+ "InstanceType" : "ml.p4d.24xlarge" ,
101+ "CurrentCount" : 2 ,
102+ "TargetCount" : 2
103+ }
104+ ]
105+ }
106+
107+ mock_sm_client .describe_cluster .return_value = cluster_response
108+
109+ # Act
110+ result = self .runner .invoke (describe_cluster , ["test-cluster" , "--region" , "us-west-2" ])
111+
112+ # Assert
113+ assert result .exit_code == 0
114+
115+ # Verify that boto3.Session was called with the correct region
116+ mock_session .assert_called_with (region_name = "us-west-2" )
117+ mock_sm_client .describe_cluster .assert_called_once_with (ClusterName = "test-cluster" )
118+ assert "📋 Cluster Details for: test-cluster" in result .output
119+ assert "test-cluster" in result .output
120+ assert "InService" in result .output
121+
122+ @patch ('sagemaker.hyperpod.cli.commands.cluster.get_sagemaker_client' )
123+ @patch ('sagemaker.hyperpod.cli.commands.cluster.boto3.Session' )
124+ @patch ('sagemaker.hyperpod.cli.commands.cluster.setup_logger' )
125+ def test_describe_cluster_unknown_cluster_name (self , mock_setup_logger , mock_session , mock_get_sagemaker_client ):
126+ """Test cluster description with unknown/non-existent cluster name."""
127+ # Arrange
128+ mock_logger = Mock ()
129+ mock_setup_logger .return_value = mock_logger
130+
131+ mock_session_instance = Mock ()
132+ mock_session .return_value = mock_session_instance
133+
134+ mock_sm_client = Mock ()
135+ mock_get_sagemaker_client .return_value = mock_sm_client
136+
137+ # Mock cluster not found exception
138+ error_response = {
139+ 'Error' : {
140+ 'Code' : 'ResourceNotFound' ,
141+ 'Message' : 'Cluster does not exist'
142+ }
143+ }
144+ mock_sm_client .describe_cluster .side_effect = ClientError (
145+ error_response , 'DescribeCluster'
146+ )
147+
148+ # Act
149+ result = self .runner .invoke (describe_cluster , ["unknown-cluster" ])
150+
151+ # Assert
152+ assert result .exit_code == 1
153+ mock_sm_client .describe_cluster .assert_called_once_with (ClusterName = "unknown-cluster" )
154+ # Should show the error message
155+ assert "❌ Cluster 'unknown-cluster' not found" in result .output
156+
157+ @patch ('sagemaker.hyperpod.cli.commands.cluster.get_sagemaker_client' )
158+ @patch ('sagemaker.hyperpod.cli.commands.cluster.boto3.Session' )
159+ @patch ('sagemaker.hyperpod.cli.commands.cluster.setup_logger' )
160+ def test_describe_cluster_access_denied (self , mock_setup_logger , mock_session , mock_get_sagemaker_client ):
161+ """Test cluster description with access denied error."""
162+ # Arrange
163+ mock_logger = Mock ()
164+ mock_setup_logger .return_value = mock_logger
165+
166+ mock_session_instance = Mock ()
167+ mock_session .return_value = mock_session_instance
168+
169+ mock_sm_client = Mock ()
170+ mock_get_sagemaker_client .return_value = mock_sm_client
171+
172+ # Mock access denied exception
173+ error_response = {
174+ 'Error' : {
175+ 'Code' : 'AccessDenied' ,
176+ 'Message' : 'User is not authorized to perform this action'
177+ }
178+ }
179+ mock_sm_client .describe_cluster .side_effect = ClientError (
180+ error_response , 'DescribeCluster'
181+ )
182+
183+ # Act
184+ result = self .runner .invoke (describe_cluster , ["test-cluster" ])
185+
186+ # Assert
187+ assert result .exit_code == 1
188+ mock_sm_client .describe_cluster .assert_called_once_with (ClusterName = "test-cluster" )
189+ # Should show the access denied message
190+ assert "❌ Access denied. Check AWS permissions" in result .output
191+
192+ @patch ('sagemaker.hyperpod.cli.commands.cluster.get_sagemaker_client' )
193+ @patch ('sagemaker.hyperpod.cli.commands.cluster.boto3.Session' )
194+ @patch ('sagemaker.hyperpod.cli.commands.cluster.setup_logger' )
195+ def test_describe_cluster_generic_error (self , mock_setup_logger , mock_session , mock_get_sagemaker_client ):
196+ """Test cluster description with generic error."""
197+ # Arrange
198+ mock_logger = Mock ()
199+ mock_setup_logger .return_value = mock_logger
200+
201+ mock_session_instance = Mock ()
202+ mock_session .return_value = mock_session_instance
203+
204+ mock_sm_client = Mock ()
205+ mock_get_sagemaker_client .return_value = mock_sm_client
206+
207+ # Mock generic exception
208+ mock_sm_client .describe_cluster .side_effect = Exception ("Unexpected error occurred" )
209+
210+ # Act
211+ result = self .runner .invoke (describe_cluster , ["test-cluster" ])
212+
213+ # Assert
214+ assert result .exit_code == 1
215+ mock_sm_client .describe_cluster .assert_called_once_with (ClusterName = "test-cluster" )
216+ # Should show the generic error message
217+ assert "❌ Error describing cluster: Unexpected error occurred" in result .output
218+
219+ @patch ('sagemaker.hyperpod.cli.commands.cluster.get_sagemaker_client' )
220+ @patch ('sagemaker.hyperpod.cli.commands.cluster.boto3.Session' )
221+ @patch ('sagemaker.hyperpod.cli.commands.cluster.setup_logger' )
222+ def test_describe_cluster_with_debug_flag (self , mock_setup_logger , mock_session , mock_get_sagemaker_client ):
223+ """Test cluster description with debug flag enabled."""
224+ # Arrange
225+ mock_logger = Mock ()
226+ mock_setup_logger .return_value = mock_logger
227+
228+ mock_session_instance = Mock ()
229+ mock_session .return_value = mock_session_instance
230+
231+ mock_sm_client = Mock ()
232+ mock_get_sagemaker_client .return_value = mock_sm_client
233+
234+ # Mock successful cluster response
235+ cluster_response = {
236+ "ClusterArn" : "arn:aws:sagemaker:us-east-2:123456789012:cluster/test-cluster" ,
237+ "ClusterName" : "test-cluster" ,
238+ "ClusterStatus" : "InService"
239+ }
240+
241+ mock_sm_client .describe_cluster .return_value = cluster_response
242+
243+ # Act
244+ result = self .runner .invoke (describe_cluster , ["test-cluster" , "--debug" ])
245+
246+ # Assert
247+ assert result .exit_code == 0
248+ mock_sm_client .describe_cluster .assert_called_once_with (ClusterName = "test-cluster" )
249+ assert "📋 Cluster Details for: test-cluster" in result .output
250+
251+ @patch ('sagemaker.hyperpod.cli.commands.cluster.get_sagemaker_client' )
252+ @patch ('sagemaker.hyperpod.cli.commands.cluster.boto3.Session' )
253+ @patch ('sagemaker.hyperpod.cli.commands.cluster.setup_logger' )
254+ def test_describe_cluster_empty_response (self , mock_setup_logger , mock_session , mock_get_sagemaker_client ):
255+ """Test cluster description with empty response."""
256+ # Arrange
257+ mock_logger = Mock ()
258+ mock_setup_logger .return_value = mock_logger
259+
260+ mock_session_instance = Mock ()
261+ mock_session .return_value = mock_session_instance
262+
263+ mock_sm_client = Mock ()
264+ mock_get_sagemaker_client .return_value = mock_sm_client
265+
266+ # Mock empty cluster response
267+ cluster_response = {}
268+
269+ mock_sm_client .describe_cluster .return_value = cluster_response
270+
271+ # Act
272+ result = self .runner .invoke (describe_cluster , ["test-cluster" ])
273+
274+ # Assert
275+ assert result .exit_code == 0
276+ mock_sm_client .describe_cluster .assert_called_once_with (ClusterName = "test-cluster" )
277+ assert "📋 Cluster Details for: test-cluster" in result .output
278+ assert "No cluster data available" in result .output
279+
280+
281+ if __name__ == "__main__" :
282+ unittest .main ()
0 commit comments