Skip to content

Commit abda708

Browse files
authored
Add databricks labs ucx delete-credential cmd to delete the UC roles created by UCX (#2504)
## Changes <!-- Summary of your changes that are easy to understand. Add screenshots when necessary --> ### Linked issues <!-- DOC: Link issue with a keyword: close, closes, closed, fix, fixes, fixed, resolve, resolves, resolved. See https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword --> Resolves #2359 https://github.com/user-attachments/assets/b0e036b5-aca9-4805-ae22-922fe6c97c1c
1 parent 89f3830 commit abda708

File tree

7 files changed

+149
-0
lines changed

7 files changed

+149
-0
lines changed

labs.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,14 @@ commands:
151151
- name: single-role
152152
description: (Optional) Create a single role for all S3 locations. (default:False)
153153

154+
- name: delete-missing-principals
155+
description: For AWS, this command identifies all the UC roles that are created through the create-missing-principals
156+
cmd. It lists all the UC roles in aws and lets users select the roles to delete. It also validates if the selected
157+
roles are used by any storage credential and prompts to confirm if roles should still be deleted.
158+
flags:
159+
- name: aws-profile
160+
description: AWS Profile to use for authentication
161+
154162
- name: create-uber-principal
155163
description: For azure cloud, creates a service principal and gives STORAGE BLOB READER access on all the storage account
156164
used by tables in the workspace and stores the spn info in the UCX cluster policy. For aws,

src/databricks/labs/ucx/assessment/aws.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,12 @@ def delete_instance_profile(self, instance_profile_name: str, role_name: str):
409409
f" --role-name {role_name}"
410410
)
411411
self._run_json_command(f"iam delete-instance-profile --instance-profile-name {instance_profile_name}")
412+
self.delete_role(role_name)
413+
414+
def delete_role(self, role_name: str):
415+
role_policies = self.list_role_policies(role_name)
416+
for policy in role_policies:
417+
self._run_json_command(f"iam delete-role-policy --role-name {role_name} --policy-name {policy}")
412418
self._run_json_command(f"iam delete-role --role-name {role_name}")
413419

414420
def add_role_to_instance_profile(self, instance_profile_name: str, role_name: str) -> bool:

src/databricks/labs/ucx/aws/access.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,3 +367,6 @@ def _generate_role_name(self, single_role: bool, role_name: str, location: str)
367367
metastore_id = self._ws.metastores.current().as_dict()["metastore_id"]
368368
return f"{role_name}_{metastore_id}"
369369
return f"{role_name}_{location[5:]}"
370+
371+
def delete_uc_role(self, role_name: str):
372+
self._aws_resources.delete_role(role_name)

src/databricks/labs/ucx/aws/credentials.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,38 @@ def run(self, prompts: Prompts, *, single_role=False, role_name="UC_ROLE", polic
230230

231231
self._resource_permissions.create_uc_roles(iam_list)
232232
self._resource_permissions.save_uc_compatible_roles()
233+
234+
def delete_uc_roles(self, prompts: Prompts) -> None:
235+
uc_roles = self._resource_permissions.load_uc_compatible_roles()
236+
if len(uc_roles) == 0:
237+
self._resource_permissions.save_uc_compatible_roles()
238+
uc_roles = self._resource_permissions.load_uc_compatible_roles()
239+
storage_credentials = self._ws.storage_credentials.list()
240+
241+
uc_role_mapping = {role.role_arn: role.role_name for role in uc_roles}
242+
selected_roles = prompts.multiple_choice_from_dict("Select the list of roles created by UCX", uc_role_mapping)
243+
if len(selected_roles) == 0:
244+
logger.info("No roles selected...")
245+
return
246+
matching_credentials = []
247+
for storage_credential in storage_credentials:
248+
if (
249+
storage_credential.aws_iam_role is not None
250+
and uc_role_mapping.get(storage_credential.aws_iam_role.role_arn) in selected_roles
251+
):
252+
matching_credentials.append(storage_credential)
253+
254+
for credential in matching_credentials:
255+
if credential.aws_iam_role is not None:
256+
logger.info(f"Storage credential: {credential.name} IAM Role: {credential.aws_iam_role.role_arn}")
257+
if len(matching_credentials) == 0:
258+
logger.info("No storage credential using the selected UC roles, proceeding to delete.")
259+
if len(matching_credentials) == 0 or prompts.confirm(
260+
"The above storage credential will be impacted on deleting the selected IAM roles,"
261+
" Are you sure you want to confirm"
262+
):
263+
264+
logger.info("Deleting UCX created roles...")
265+
for role_name in selected_roles:
266+
logger.info(f"Deleting role {role_name}.")
267+
self._resource_permissions.delete_uc_role(role_name)

src/databricks/labs/ucx/cli.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,25 @@ def create_missing_principals(
347347
raise ValueError("Unsupported cloud provider")
348348

349349

350+
@ucx.command
351+
def delete_missing_principals(
352+
w: WorkspaceClient,
353+
prompts: Prompts,
354+
ctx: WorkspaceContext | None = None,
355+
**named_parameters,
356+
):
357+
"""Not supported for Azure.
358+
For AWS, this command identifies all the UC roles that are created through the create-missing-principals cmd.
359+
It lists all the UC roles in aws and lets users select the roles to delete. It also validates if the selected roles
360+
are used by any storage credential and prompts to confirm if roles should still be deleted.
361+
"""
362+
if not ctx:
363+
ctx = WorkspaceContext(w, named_parameters)
364+
if ctx.is_aws:
365+
return ctx.iam_role_creation.delete_uc_roles(prompts)
366+
raise ValueError("Unsupported cloud provider")
367+
368+
350369
@ucx.command
351370
def migrate_credentials(w: WorkspaceClient, prompts: Prompts, ctx: WorkspaceContext | None = None, **named_parameters):
352371
"""For Azure, this command prompts to i) create UC storage credentials for the access connectors with a

tests/unit/aws/test_access.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,3 +864,69 @@ def command_call(_: str):
864864
assert len(roles) == 1
865865
assert len(roles[0].paths) == 2
866866
external_locations.snapshot.assert_called_once()
867+
868+
869+
def test_delete_uc_roles(mock_ws, installation_multiple_roles, backend, locations):
870+
aws = create_autospec(AWSResources)
871+
aws.validate_connection.return_value = {}
872+
aws_resource_permissions = AWSResourcePermissions(installation_multiple_roles, mock_ws, aws, locations)
873+
mock_ws.storage_credentials.list.return_value = [
874+
StorageCredentialInfo(
875+
id="1",
876+
name="cred1",
877+
aws_iam_role=AwsIamRoleResponse("arn:aws:iam::12345:role/uc-role1"),
878+
)
879+
]
880+
role_creation = IamRoleCreation(installation_multiple_roles, mock_ws, aws_resource_permissions)
881+
prompts = MockPrompts({"Select the list of roles *": "1", "The above storage credential will be impacted *": "Yes"})
882+
role_creation.delete_uc_roles(prompts)
883+
calls = [call("uc-role1"), call("uc-rolex")]
884+
assert aws.delete_role.mock_calls == calls
885+
886+
887+
def test_delete_uc_roles_not_present(mock_ws, installation_no_roles, backend, locations):
888+
aws = create_autospec(AWSResources)
889+
aws.validate_connection.return_value = {}
890+
aws.delete_role.return_value = []
891+
aws_resource_permissions = AWSResourcePermissions(installation_no_roles, mock_ws, aws, locations)
892+
mock_ws.storage_credentials.list.return_value = [
893+
StorageCredentialInfo(
894+
id="1",
895+
name="cred1",
896+
aws_iam_role=AwsIamRoleResponse("arn:aws:iam::12345:role/uc-role1"),
897+
)
898+
]
899+
role_creation = IamRoleCreation(installation_no_roles, mock_ws, aws_resource_permissions)
900+
aws.list_all_uc_roles.return_value = [AWSRole("", "uc-role1", "123", "arn:aws:iam::12345:role/uc-role1")]
901+
aws.get_role_policy.side_effect = [
902+
[
903+
AWSPolicyAction(
904+
resource_type="s3",
905+
privilege="READ_FILES",
906+
resource_path="s3://bucket1",
907+
)
908+
]
909+
]
910+
aws.list_role_policies.return_value = ["Policy1"]
911+
aws.list_all_uc_roles.return_value = [
912+
AWSRole(path='/', role_name='uc-role1', role_id='12345', arn='arn:aws:iam::12345:role/uc-role1')
913+
]
914+
prompts = MockPrompts({"Select the list of roles *": "1", "The above storage credential will be impacted *": "Yes"})
915+
role_creation.delete_uc_roles(prompts)
916+
calls = [call("uc-role1")]
917+
assert aws.delete_role.mock_calls == calls
918+
919+
920+
def test_delete_role(mock_ws, installation_no_roles, backend, mocker):
921+
command_calls = []
922+
mocker.patch("shutil.which", return_value="/path/aws")
923+
924+
def command_call(cmd: str):
925+
command_calls.append(cmd)
926+
return 0, '{"account":"1234"}', ""
927+
928+
aws = AWSResources("profile", command_call)
929+
external_locations = ExternalLocations(mock_ws, backend, 'ucx')
930+
resource_permissions = AWSResourcePermissions(installation_no_roles, mock_ws, aws, external_locations)
931+
resource_permissions.delete_uc_role("uc_role_1")
932+
assert '/path/aws iam delete-role --role-name uc_role_1 --profile profile --output json' in command_calls

tests/unit/test_cli.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import pytest
88
import yaml
99
from databricks.labs.blueprint.tui import MockPrompts
10+
from databricks.labs.ucx.aws.credentials import IamRoleCreation
1011
from databricks.sdk import AccountClient, WorkspaceClient
1112
from databricks.sdk.errors import NotFound
1213
from databricks.sdk.errors.platform import BadRequest
@@ -53,6 +54,7 @@
5354
validate_external_locations,
5455
validate_groups_membership,
5556
workflows,
57+
delete_missing_principals,
5658
)
5759
from databricks.labs.ucx.contexts.account_cli import AccountContext
5860
from databricks.labs.ucx.contexts.workspace_cli import WorkspaceContext
@@ -742,3 +744,13 @@ def test_join_collection():
742744
w.workspace.download.return_value = io.StringIO(json.dumps([{"workspace_id": 123, "workspace_name": "some"}]))
743745
join_collection(a, "123")
744746
w.workspace.download.assert_not_called()
747+
748+
749+
def test_delete_principals(ws):
750+
ws.config.is_azure = False
751+
ws.config.is_aws = True
752+
role_creation = create_autospec(IamRoleCreation)
753+
ctx = WorkspaceContext(ws).replace(iam_role_creation=role_creation, workspace_client=ws)
754+
prompts = MockPrompts({"Select the list of roles *": "0"})
755+
delete_missing_principals(ws, prompts, ctx)
756+
role_creation.delete_uc_roles.assert_called_once()

0 commit comments

Comments
 (0)