Skip to content

Commit b7cc950

Browse files
pritishpainfx
andauthored
Total Storage Credentials count widget for Assessment Dashboard (#2201)
## Changes Adding a widget to show the current total storage credentials created in the workspace (limit 200) ### Linked issues Introduces #1600 ### Functionality - [ ] added relevant user documentation - [ ] added new CLI command - [ ] modified existing command: `databricks labs ucx ...` - [ ] added a new workflow - [ ] modified existing workflow: `...` - [ ] added a new table - [ ] modified existing table: `...` ### Tests - [x] manually tested - [ ] added unit tests - [ ] added integration tests - [ ] verified on staging environment (screenshot attached) --------- Co-authored-by: Serge Smertin <[email protected]>
1 parent 5d5cbc9 commit b7cc950

File tree

8 files changed

+123
-16
lines changed

8 files changed

+123
-16
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,8 @@ def get_roles_to_migrate(self) -> list[AWSCredentialCandidate]:
222222
roles[role.role_arn].paths.add(external_location.location)
223223
if role.privilege == Privilege.WRITE_FILES.value:
224224
roles[role.role_arn].privilege = Privilege.WRITE_FILES.value
225-
225+
if len(roles) > 200:
226+
raise RuntimeWarning('Migration will breach UC limits (Storage Credentials > 200).')
226227
return list(roles.values())
227228

228229
def _get_cluster_policy(self, policy_id: str | None) -> Policy:

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,8 @@ def create_access_connectors_for_storage_accounts(self) -> list[tuple[AccessConn
317317
list[AccessConnector, str] : The access connectors with a storage url to which it has access.
318318
"""
319319
used_storage_accounts = self._get_storage_accounts()
320+
if len(used_storage_accounts) > 200:
321+
raise RuntimeWarning('Migration will breach UC limits (Storage Credentials > 200).')
320322
if len(used_storage_accounts) == 0:
321323
logger.warning(
322324
"There are no external table present with azure storage account. "
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/* --title 'Total Storage Credentials'
2+
--height 3
3+
--overrides '{
4+
"spec": {
5+
"encodings": {
6+
"value": {
7+
"fieldName": "count_total_storage_credentials",
8+
"rowNumber": 2,
9+
"style": {
10+
"rules": [
11+
{
12+
"condition": {
13+
"operator": ">",
14+
"operand": {
15+
"type": "data-value",
16+
"value": "200"
17+
}
18+
},
19+
"color": "#E92828"
20+
}
21+
]
22+
},
23+
"displayName": "count_total_storage_credentials"
24+
}
25+
}
26+
}
27+
}'
28+
*/
29+
SELECT
30+
COUNT(*) AS count_total_storage_credentials
31+
FROM inventory.external_locations
Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
/* --title 'UC readiness' --height 4 */
2-
WITH raw AS (
1+
/* --title 'UC readiness' --height 3 */
2+
SELECT
3+
COALESCE(CONCAT(ROUND(SUM(ready) / COUNT(*) * 100, 1), '%'), 'N/A') AS readiness
4+
FROM (
35
SELECT
46
object_type,
57
object_id,
68
IF(failures = '[]', 1, 0) AS ready
79
FROM inventory.objects
8-
)
9-
SELECT
10-
COALESCE(CONCAT(ROUND(SUM(ready) / COUNT(*) * 100, 1), '%'), 'N/A') AS readiness
11-
FROM raw
10+
)

src/databricks/labs/ucx/queries/assessment/main/01_1_count_jobs.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* --title 'Total Jobs' --height 4 */
1+
/* --title 'Total Jobs' --width 1 --height 3 */
22
SELECT
33
COUNT(*) AS count_total_jobs
44
FROM inventory.jobs

src/databricks/labs/ucx/queries/assessment/main/01_2_count_table_by_storage.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* --title 'Table counts by storage' --width 2 --height 4 */
1+
/* --title 'Table counts by storage' --width 1 --height 3 */
22
SELECT
33
storage,
44
COUNT(*) AS count

src/databricks/labs/ucx/queries/assessment/main/01_3_count_table_by_type_location_format.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* --title 'Table counts by type, location and format' --width 2 --height 4 */
1+
/* --title 'Table counts by type, location and format' --width 2 --height 3 */
22
SELECT
33
object_type AS source_table_type,
44
table_format AS format,

tests/unit/test_cli.py

Lines changed: 80 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,21 @@
22
import json
33
import time
44
from pathlib import Path
5-
from unittest.mock import create_autospec, patch
5+
from unittest.mock import create_autospec, patch, Mock
66

77
import pytest
88
import yaml
9-
from databricks.sdk.service.provisioning import Workspace
109
from databricks.labs.blueprint.tui import MockPrompts
1110
from databricks.sdk import AccountClient, WorkspaceClient
1211
from databricks.sdk.errors import NotFound
12+
from databricks.sdk.errors.platform import BadRequest
1313
from databricks.sdk.service import iam, jobs, sql
1414
from databricks.sdk.service.catalog import ExternalLocationInfo
1515
from databricks.sdk.service.compute import ClusterDetails, ClusterSource
16+
from databricks.sdk.service.provisioning import Workspace
1617
from databricks.sdk.service.workspace import ObjectInfo, ObjectType
17-
from databricks.sdk.errors.platform import BadRequest
1818

19-
from databricks.labs.ucx.assessment.aws import AWSResources
19+
from databricks.labs.ucx.assessment.aws import AWSResources, AWSRoleAction
2020
from databricks.labs.ucx.aws.access import AWSResourcePermissions
2121
from databricks.labs.ucx.azure.access import AzureResourcePermissions
2222
from databricks.labs.ucx.azure.resources import AzureResources
@@ -31,6 +31,7 @@
3131
create_uber_principal,
3232
ensure_assessment_run,
3333
installations,
34+
join_collection,
3435
logs,
3536
manual_workspace_info,
3637
migrate_credentials,
@@ -51,11 +52,11 @@
5152
validate_external_locations,
5253
validate_groups_membership,
5354
workflows,
54-
join_collection,
5555
)
5656
from databricks.labs.ucx.contexts.account_cli import AccountContext
5757
from databricks.labs.ucx.contexts.workspace_cli import WorkspaceContext
58-
from databricks.labs.ucx.hive_metastore import TablesCrawler
58+
from databricks.labs.ucx.hive_metastore import TablesCrawler, ExternalLocations
59+
from databricks.labs.ucx.hive_metastore.locations import ExternalLocation
5960
from databricks.labs.ucx.hive_metastore.tables import Table
6061
from databricks.labs.ucx.source_code.linters.files import LocalFileMigrator
6162

@@ -364,6 +365,79 @@ def test_migrate_credentials_aws(ws):
364365
ws.storage_credentials.list.assert_called()
365366

366367

368+
def test_migrate_credentials_limit_azure(ws):
369+
azure_resources = create_autospec(AzureResources)
370+
external_locations = create_autospec(ExternalLocations)
371+
external_locations_mock = []
372+
for i in range(200):
373+
external_locations_mock.append(
374+
ExternalLocation(
375+
location=f"abfss://container{i}@storage{i}.dfs.core.windows.net/folder{i}", table_count=i % 20
376+
)
377+
)
378+
external_locations.snapshot.return_value = external_locations_mock
379+
prompts = MockPrompts({'.*': 'yes'})
380+
ctx = WorkspaceContext(ws).replace(
381+
is_azure=True,
382+
azure_cli_authenticated=True,
383+
azure_subscription_id='test',
384+
azure_resources=azure_resources,
385+
external_locations=external_locations,
386+
)
387+
migrate_credentials(ws, prompts, ctx=ctx)
388+
ws.storage_credentials.list.assert_called()
389+
azure_resources.storage_accounts.assert_called()
390+
391+
external_locations_mock.append(
392+
ExternalLocation(
393+
location=f"abfss://container{201}@storage{201}.dfs.core.windows.net/folder{201}", table_count=25
394+
)
395+
)
396+
with pytest.raises(RuntimeWarning):
397+
migrate_credentials(ws, prompts, ctx=ctx)
398+
399+
400+
def test_migrate_credentials_limit_aws(ws):
401+
aws_resources = create_autospec(AWSResources)
402+
external_locations = create_autospec(ExternalLocations)
403+
404+
external_locations_mock = []
405+
aws_role_actions_mock = []
406+
for i in range(200):
407+
location = f"s3://labsawsbucket/{i}"
408+
role = f"arn:aws:iam::123456789012:role/role_name{i}"
409+
external_locations_mock.append(ExternalLocation(location=location, table_count=i % 20))
410+
aws_role_actions_mock.append(
411+
AWSRoleAction(
412+
role_arn=role,
413+
privilege="READ_FILES",
414+
resource_path=location,
415+
resource_type="s3",
416+
)
417+
)
418+
external_locations.snapshot.return_value = external_locations_mock
419+
aws_resources.validate_connection.return_value = {"Account": "123456789012"}
420+
421+
prompts = MockPrompts({'.*': 'yes'})
422+
AWSResourcePermissions.load_uc_compatible_roles = Mock()
423+
AWSResourcePermissions.load_uc_compatible_roles.return_value = aws_role_actions_mock
424+
ctx = WorkspaceContext(ws).replace(is_aws=True, aws_resources=aws_resources, external_locations=external_locations)
425+
migrate_credentials(ws, prompts, ctx=ctx)
426+
ws.storage_credentials.list.assert_called()
427+
428+
external_locations_mock.append(ExternalLocation(location="s3://labsawsbucket/201", table_count=25))
429+
aws_role_actions_mock.append(
430+
AWSRoleAction(
431+
role_arn="arn:aws:iam::123456789012:role/role_name201",
432+
privilege="READ_FILES",
433+
resource_path="s3://labsawsbucket/201",
434+
resource_type="s3",
435+
)
436+
)
437+
with pytest.raises(RuntimeWarning):
438+
migrate_credentials(ws, prompts, ctx=ctx)
439+
440+
367441
def test_create_master_principal_not_azure(ws):
368442
ws.config.is_azure = False
369443
ws.config.is_aws = False

0 commit comments

Comments
 (0)