Skip to content

Commit 98f9571

Browse files
authored
Automatically upgrade existing installations to avoid breaking changes (#985)
This PR incorporates the work from databrickslabs/blueprint#50, which enables smoother cross-version upgrades. Fix #471
1 parent 4ae825b commit 98f9571

File tree

3 files changed

+130
-30
lines changed

3 files changed

+130
-30
lines changed

src/databricks/labs/ucx/install.py

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import sys
77
import time
88
import webbrowser
9+
from collections.abc import Callable
910
from dataclasses import replace
1011
from datetime import datetime, timedelta
1112
from pathlib import Path
@@ -16,6 +17,7 @@
1617
from databricks.labs.blueprint.installer import InstallState
1718
from databricks.labs.blueprint.parallel import ManyError, Threads
1819
from databricks.labs.blueprint.tui import Prompts
20+
from databricks.labs.blueprint.upgrades import Upgrades
1921
from databricks.labs.blueprint.wheels import ProductInfo, WheelsV2, find_project_root
2022
from databricks.sdk import WorkspaceClient
2123
from databricks.sdk.errors import ( # pylint: disable=redefined-builtin
@@ -176,54 +178,65 @@ def __init__(self, prompts: Prompts, installation: Installation, ws: WorkspaceCl
176178
self._installation = installation
177179
self._prompts = prompts
178180

179-
def run(self):
181+
def run(
182+
self,
183+
verify_timeout=timedelta(minutes=2),
184+
sql_backend_factory: Callable[[WorkspaceConfig], SqlBackend] | None = None,
185+
wheel_builder_factory: Callable[[], WheelsV2] | None = None,
186+
):
180187
logger.info(f"Installing UCX v{PRODUCT_INFO.version()}")
181188
config = self.configure()
182-
sql_backend = StatementExecutionBackend(self._ws, config.warehouse_id)
183-
wheels = WheelsV2(self._installation, PRODUCT_INFO)
189+
if not sql_backend_factory:
190+
sql_backend_factory = self._new_sql_backend
191+
if not wheel_builder_factory:
192+
wheel_builder_factory = self._new_wheel_builder
184193
workspace_installation = WorkspaceInstallation(
185194
config,
186195
self._installation,
187-
sql_backend,
188-
wheels,
196+
sql_backend_factory(config),
197+
wheel_builder_factory(),
189198
self._ws,
190199
self._prompts,
191-
verify_timeout=timedelta(minutes=2),
200+
verify_timeout=verify_timeout,
192201
)
193-
workspace_installation.run()
202+
try:
203+
workspace_installation.run()
204+
except ManyError as err:
205+
if len(err.errs) == 1:
206+
raise err.errs[0] from None
207+
raise err
208+
209+
def _new_wheel_builder(self):
210+
return WheelsV2(self._installation, PRODUCT_INFO)
211+
212+
def _new_sql_backend(self, config: WorkspaceConfig) -> SqlBackend:
213+
return StatementExecutionBackend(self._ws, config.warehouse_id)
194214

195215
def configure(self) -> WorkspaceConfig:
196216
try:
197-
return self._installation.load(WorkspaceConfig)
217+
config = self._installation.load(WorkspaceConfig)
218+
self._apply_upgrades()
219+
return config
198220
except NotFound as err:
199221
logger.debug(f"Cannot find previous installation: {err}")
222+
return self._configure_new_installation()
223+
224+
def _apply_upgrades(self):
225+
try:
226+
upgrades = Upgrades(PRODUCT_INFO, self._installation)
227+
upgrades.apply(self._ws)
228+
except NotFound as err:
229+
logger.warning(f"Installed version is too old: {err}")
230+
return
231+
232+
def _configure_new_installation(self) -> WorkspaceConfig:
200233
logger.info("Please answer a couple of questions to configure Unity Catalog migration")
201234
HiveMetastoreLineageEnabler(self._ws).apply(self._prompts)
202235
inventory_database = self._prompts.question(
203236
"Inventory Database stored in hive_metastore", default="ucx", valid_regex=r"^\w+$"
204237
)
205238

206-
def warehouse_type(_):
207-
return _.warehouse_type.value if not _.enable_serverless_compute else "SERVERLESS"
208-
209-
pro_warehouses = {"[Create new PRO SQL warehouse]": "create_new"} | {
210-
f"{_.name} ({_.id}, {warehouse_type(_)}, {_.state.value})": _.id
211-
for _ in self._ws.warehouses.list()
212-
if _.warehouse_type == EndpointInfoWarehouseType.PRO
213-
}
214-
warehouse_id = self._prompts.choice_from_dict(
215-
"Select PRO or SERVERLESS SQL warehouse to run assessment dashboards on", pro_warehouses
216-
)
217-
if warehouse_id == "create_new":
218-
new_warehouse = self._ws.warehouses.create(
219-
name=f"{WAREHOUSE_PREFIX} {time.time_ns()}",
220-
spot_instance_policy=SpotInstancePolicy.COST_OPTIMIZED,
221-
warehouse_type=CreateWarehouseRequestWarehouseType.PRO,
222-
cluster_size="Small",
223-
max_num_clusters=1,
224-
)
225-
warehouse_id = new_warehouse.id
226-
239+
warehouse_id = self._configure_warehouse()
227240
configure_groups = ConfigureGroups(self._prompts)
228241
configure_groups.run()
229242
log_level = self._prompts.question("Log level", default="INFO").upper()
@@ -269,6 +282,29 @@ def warehouse_type(_):
269282
webbrowser.open(ws_file_url)
270283
return config
271284

285+
def _configure_warehouse(self):
286+
def warehouse_type(_):
287+
return _.warehouse_type.value if not _.enable_serverless_compute else "SERVERLESS"
288+
289+
pro_warehouses = {"[Create new PRO SQL warehouse]": "create_new"} | {
290+
f"{_.name} ({_.id}, {warehouse_type(_)}, {_.state.value})": _.id
291+
for _ in self._ws.warehouses.list()
292+
if _.warehouse_type == EndpointInfoWarehouseType.PRO
293+
}
294+
warehouse_id = self._prompts.choice_from_dict(
295+
"Select PRO or SERVERLESS SQL warehouse to run assessment dashboards on", pro_warehouses
296+
)
297+
if warehouse_id == "create_new":
298+
new_warehouse = self._ws.warehouses.create(
299+
name=f"{WAREHOUSE_PREFIX} {time.time_ns()}",
300+
spot_instance_policy=SpotInstancePolicy.COST_OPTIMIZED,
301+
warehouse_type=CreateWarehouseRequestWarehouseType.PRO,
302+
cluster_size="Small",
303+
max_num_clusters=1,
304+
)
305+
warehouse_id = new_warehouse.id
306+
return warehouse_id
307+
272308
@staticmethod
273309
def _policy_config(value: str):
274310
return {"type": "fixed", "value": value}
@@ -370,7 +406,7 @@ def __init__(
370406

371407
@classmethod
372408
def current(cls, ws: WorkspaceClient):
373-
installation = Installation.current(ws, PRODUCT_INFO.product_name())
409+
installation = PRODUCT_INFO.current_installation(ws)
374410
config = installation.load(WorkspaceConfig)
375411
sql_backend = StatementExecutionBackend(ws, config.warehouse_id)
376412
wheels = WheelsV2(installation, PRODUCT_INFO)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# pylint: disable=invalid-name,unused-argument
2+
import logging
3+
4+
from databricks.labs.blueprint.installation import Installation
5+
from databricks.sdk import WorkspaceClient
6+
7+
logger = logging.getLogger(__name__)
8+
9+
10+
def upgrade(installation: Installation, ws: WorkspaceClient):
11+
installation.upload('logs/README.md', b'# This folder contains logs from UCX workflows')

tests/unit/test_install.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,3 +1339,56 @@ def test_open_config(ws, mocker, mock_installation):
13391339
install.configure()
13401340

13411341
webbrowser_open.assert_called_with('https://localhost/#workspace~/mock/config.yml')
1342+
1343+
1344+
def test_runs_upgrades_on_too_old_version(ws, any_prompt):
1345+
existing_installation = MockInstallation(
1346+
{
1347+
'state.json': {'resources': {'dashboards': {'assessment_main': 'abc'}}},
1348+
'config.yml': {
1349+
'inventory_database': 'x',
1350+
'warehouse_id': 'abc',
1351+
'connect': {'host': '...', 'token': '...'},
1352+
},
1353+
}
1354+
)
1355+
install = WorkspaceInstaller(any_prompt, existing_installation, ws)
1356+
1357+
sql_backend = MockBackend()
1358+
wheels = create_autospec(WheelsV2)
1359+
1360+
# TODO: (HariGS-DB) remove this, once added the policy upgrade
1361+
# TODO: fix along https://github.com/databrickslabs/ucx/issues/1012
1362+
with pytest.raises(InvalidParameterValue):
1363+
install.run(
1364+
verify_timeout=timedelta(seconds=1),
1365+
sql_backend_factory=lambda _: sql_backend,
1366+
wheel_builder_factory=lambda: wheels,
1367+
)
1368+
1369+
1370+
def test_runs_upgrades_on_more_recent_version(ws, any_prompt):
1371+
existing_installation = MockInstallation(
1372+
{
1373+
'version.json': {'version': '0.3.0', 'wheel': '...', 'date': '...'},
1374+
'state.json': {'resources': {'dashboards': {'assessment_main': 'abc'}}},
1375+
'config.yml': {
1376+
'inventory_database': 'x',
1377+
'warehouse_id': 'abc',
1378+
'policy_id': 'abc', # TODO: (HariGS-DB) remove this, once added the policy upgrade
1379+
'connect': {'host': '...', 'token': '...'},
1380+
},
1381+
}
1382+
)
1383+
install = WorkspaceInstaller(any_prompt, existing_installation, ws)
1384+
1385+
sql_backend = MockBackend()
1386+
wheels = create_autospec(WheelsV2)
1387+
1388+
install.run(
1389+
verify_timeout=timedelta(seconds=1),
1390+
sql_backend_factory=lambda _: sql_backend,
1391+
wheel_builder_factory=lambda: wheels,
1392+
)
1393+
1394+
existing_installation.assert_file_uploaded('logs/README.md')

0 commit comments

Comments
 (0)