Skip to content

Commit 9325414

Browse files
authored
Skip installation if remote and local version is the same, provide prompt to override (#1084)
## Changes Compares the remote version and local wheel version and prompts for re-install confirmation if they are the same. ### Linked issues Closes #1072 Closes #803 ### Functionality - [ ] added relevant user documentation - [ ] added new CLI command - [ ] modified existing command: `databricks labs ucx ...` - [ ] added a new workflow - [x] modified existing workflow: `...` - [ ] added a new table - [ ] modified existing table: `...` ### Tests <!-- How is this tested? Please see the checklist below and also describe any other relevant tests --> - [ ] manually tested - [x] added unit tests - [ ] added integration tests - [ ] verified on staging environment (screenshot attached)
1 parent 807ebff commit 9325414

File tree

3 files changed

+131
-11
lines changed

3 files changed

+131
-11
lines changed

src/databricks/labs/ucx/install.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import functools
22
import logging
33
import os
4+
import re
45
import time
56
import webbrowser
67
from collections.abc import Callable
@@ -13,7 +14,12 @@
1314
from databricks.labs.blueprint.parallel import ManyError, Threads
1415
from databricks.labs.blueprint.tui import Prompts
1516
from databricks.labs.blueprint.upgrades import Upgrades
16-
from databricks.labs.blueprint.wheels import ProductInfo, WheelsV2, find_project_root
17+
from databricks.labs.blueprint.wheels import (
18+
ProductInfo,
19+
Version,
20+
WheelsV2,
21+
find_project_root,
22+
)
1723
from databricks.labs.lsql.backends import SqlBackend, StatementExecutionBackend
1824
from databricks.labs.lsql.deployment import SchemaDeployer
1925
from databricks.sdk import WorkspaceClient
@@ -91,6 +97,13 @@ def deploy_schema(sql_backend: SqlBackend, inventory_schema: str):
9197
deployer.deploy_view("table_estimates", "queries/views/table_estimates.sql")
9298

9399

100+
def extract_major_minor(version_string):
101+
match = re.search(r'(\d+\.\d+)', version_string)
102+
if match:
103+
return match.group(1)
104+
return None
105+
106+
94107
class WorkspaceInstaller:
95108
def __init__(
96109
self,
@@ -144,6 +157,20 @@ def run(
144157
raise err.errs[0] from None
145158
raise err
146159

160+
def _compare_remote_local_versions(self):
161+
try:
162+
local_version = self._product_info.released_version()
163+
remote_version = self._installation.load(Version).version
164+
if extract_major_minor(remote_version) == extract_major_minor(local_version):
165+
logger.info(f"UCX v{self._product_info.version()} is already installed on this workspace")
166+
msg = "Do you want to update the existing installation?"
167+
if not self._prompts.confirm(msg):
168+
raise RuntimeWarning(
169+
"UCX workspace remote and local install versions are same and no override is requested. Exiting..."
170+
)
171+
except NotFound as err:
172+
logger.warning(f"UCX workspace remote version not found: {err}")
173+
147174
def _new_wheel_builder(self):
148175
return WheelsV2(self._installation, self._product_info)
149176

@@ -171,6 +198,7 @@ def _confirm_force_install(self) -> bool:
171198
def configure(self) -> WorkspaceConfig:
172199
try:
173200
config = self._installation.load(WorkspaceConfig)
201+
self._compare_remote_local_versions()
174202
if self._confirm_force_install():
175203
return self._configure_new_installation()
176204
self._apply_upgrades()

tests/integration/test_installation.py

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ def factory(
5353
if not environ:
5454
environ = {}
5555
renamed_group_prefix = f"rename-{product_info.product_name()}-"
56-
5756
prompts = MockPrompts(
5857
{
5958
r'Open job overview in your browser.*': 'no',
@@ -377,6 +376,9 @@ def test_global_installation_on_existing_global_install(ws, new_installation):
377376
reinstall_global, _ = new_installation(
378377
product_info=product_info,
379378
installation=Installation.assume_global(ws, product_info.product_name()),
379+
extend_prompts={
380+
r".*Do you want to update the existing installation?.*": 'yes',
381+
},
380382
)
381383
assert reinstall_global.folder == f"/Applications/{product_info.product_name()}"
382384
reinstall_global.uninstall()
@@ -391,16 +393,16 @@ def test_user_installation_on_existing_global_install(ws, new_installation):
391393
)
392394

393395
# warning to be thrown by installer if override environment variable present but no confirmation
394-
with pytest.raises(RuntimeWarning) as err:
396+
with pytest.raises(RuntimeWarning, match="UCX is already installed, but no confirmation"):
395397
new_installation(
396398
product_info=product_info,
397399
installation=Installation.assume_global(ws, product_info.product_name()),
398400
environ={'UCX_FORCE_INSTALL': 'user'},
399401
extend_prompts={
400402
r".*UCX is already installed on this workspace.*": 'no',
403+
r".*Do you want to update the existing installation?.*": 'yes',
401404
},
402405
)
403-
assert err.value.args[0] == "UCX is already installed, but no confirmation"
404406

405407
# successful override with confirmation
406408
reinstall_user_force, _ = new_installation(
@@ -409,6 +411,7 @@ def test_user_installation_on_existing_global_install(ws, new_installation):
409411
environ={'UCX_FORCE_INSTALL': 'user'},
410412
extend_prompts={
411413
r".*UCX is already installed on this workspace.*": 'yes',
414+
r".*Do you want to update the existing installation?.*": 'yes',
412415
},
413416
inventory_schema_suffix="_reinstall",
414417
)
@@ -428,28 +431,28 @@ def test_global_installation_on_existing_user_install(ws, new_installation):
428431
)
429432

430433
# warning to be thrown by installer if override environment variable present but no confirmation
431-
with pytest.raises(RuntimeWarning) as err:
434+
with pytest.raises(RuntimeWarning, match="UCX is already installed, but no confirmation"):
432435
new_installation(
433436
product_info=product_info,
434437
installation=Installation.assume_user_home(ws, product_info.product_name()),
435438
environ={'UCX_FORCE_INSTALL': 'global'},
436439
extend_prompts={
437440
r".*UCX is already installed on this workspace.*": 'no',
441+
r".*Do you want to update the existing installation?.*": 'yes',
438442
},
439443
)
440-
assert err.value.args[0] == "UCX is already installed, but no confirmation"
441444

442445
# not implemented error with confirmation
443-
with pytest.raises(databricks.sdk.errors.NotImplemented) as err:
446+
with pytest.raises(databricks.sdk.errors.NotImplemented, match="Migration needed. Not implemented yet."):
444447
new_installation(
445448
product_info=product_info,
446449
installation=Installation.assume_user_home(ws, product_info.product_name()),
447450
environ={'UCX_FORCE_INSTALL': 'global'},
448451
extend_prompts={
449452
r".*UCX is already installed on this workspace.*": 'yes',
453+
r".*Do you want to update the existing installation?.*": 'yes',
450454
},
451455
)
452-
assert err.value.args[0] == "Migration needed. Not implemented yet."
453456
existing_user_installation.uninstall()
454457

455458

@@ -461,16 +464,18 @@ def test_check_inventory_database_exists(ws, new_installation):
461464
)
462465
inventory_database = install.config.inventory_database
463466

464-
with pytest.raises(AlreadyExists) as err:
467+
with pytest.raises(
468+
AlreadyExists, match=f"Inventory database '{inventory_database}' already exists in another installation"
469+
):
465470
new_installation(
466471
product_info=product_info,
467472
installation=Installation.assume_global(ws, product_info.product_name()),
468473
environ={'UCX_FORCE_INSTALL': 'user'},
469474
extend_prompts={
470475
r".*UCX is already installed on this workspace.*": 'yes',
476+
r".*Do you want to update the existing installation?.*": 'yes',
471477
},
472478
)
473-
assert err.value.args[0] == f"Inventory database '{inventory_database}' already exists in another installation"
474479

475480

476481
@retried(on=[NotFound], timeout=timedelta(minutes=10))
@@ -616,3 +621,20 @@ def test_partitioned_tables(ws, sql_backend, new_installation, inventory_schema,
616621
assert len(all_tables) >= 2
617622
assert all_tables[f"{schema.full_name}.partitioned_table"].is_partitioned is True
618623
assert all_tables[f"{schema.full_name}.non_partitioned_table"].is_partitioned is False
624+
625+
626+
def test_compare_remote_local_install_versions(ws, new_installation):
627+
product_info = ProductInfo.for_testing(WorkspaceConfig)
628+
new_installation(product_info=product_info)
629+
with pytest.raises(
630+
RuntimeWarning,
631+
match="UCX workspace remote and local install versions are same and no override is requested. Exiting...",
632+
):
633+
new_installation(product_info=product_info)
634+
635+
new_installation(
636+
product_info=product_info,
637+
extend_prompts={
638+
r".*Do you want to update the existing installation?.*": 'yes',
639+
},
640+
)

tests/unit/test_install.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@
5555
import databricks.labs.ucx.uninstall # noqa
5656
from databricks.labs.ucx.config import WorkspaceConfig
5757
from databricks.labs.ucx.framework.dashboards import DashboardFromFiles
58-
from databricks.labs.ucx.install import WorkspaceInstallation, WorkspaceInstaller
58+
from databricks.labs.ucx.install import (
59+
WorkspaceInstallation,
60+
WorkspaceInstaller,
61+
extract_major_minor,
62+
)
5963
from databricks.labs.ucx.installer.workflows import WorkflowsInstallation
6064

6165
PRODUCT_INFO = ProductInfo.from_class(WorkspaceConfig)
@@ -1497,3 +1501,69 @@ def test_validate_step(ws, any_prompt, result_state, expected):
14971501
)
14981502

14991503
assert workflows_installer.validate_step("assessment") == expected
1504+
1505+
1506+
def test_are_remote_local_versions_equal(ws, mock_installation, mocker):
1507+
ws.jobs.run_now = mocker.Mock()
1508+
1509+
mocker.patch("webbrowser.open")
1510+
base_prompts = MockPrompts(
1511+
{
1512+
r"Open config file in.*": "yes",
1513+
r"Open job overview in your browser.*": "yes",
1514+
r"Do you want to trigger assessment job ?.*": "yes",
1515+
r"Open assessment Job url that just triggered ?.*": "yes",
1516+
r".*": "",
1517+
}
1518+
)
1519+
1520+
product_info = create_autospec(ProductInfo)
1521+
product_info.released_version.return_value = "0.3.0"
1522+
1523+
installation = MockInstallation(
1524+
{
1525+
'config.yml': {
1526+
'inventory_database': 'ucx_user',
1527+
'connect': {
1528+
'host': '...',
1529+
'token': '...',
1530+
},
1531+
},
1532+
'version.json': {'version': '0.3.0', 'wheel': '...', 'date': '...'},
1533+
},
1534+
is_global=False,
1535+
)
1536+
1537+
install = WorkspaceInstaller(base_prompts, installation, ws, product_info)
1538+
1539+
# raises runtime warning when versions match and no override provided
1540+
with pytest.raises(
1541+
RuntimeWarning,
1542+
match="UCX workspace remote and local install versions are same and no override is requested. Exiting...",
1543+
):
1544+
install.configure()
1545+
1546+
first_prompts = base_prompts.extend(
1547+
{
1548+
r"Do you want to update the existing installation?": "yes",
1549+
}
1550+
)
1551+
install = WorkspaceInstaller(first_prompts, installation, ws, product_info)
1552+
1553+
# finishes successfully when versions match and override is provided
1554+
config = install.configure()
1555+
assert config.inventory_database == "ucx_user"
1556+
1557+
# finishes successfully when versions don't match and no override is provided/needed
1558+
product_info.released_version.return_value = "0.4.1"
1559+
install = WorkspaceInstaller(base_prompts, installation, ws, product_info)
1560+
config = install.configure()
1561+
assert config.inventory_database == "ucx_user"
1562+
1563+
1564+
def test_extract_major_minor_versions():
1565+
version_string1 = "0.3.123151"
1566+
version_string2 = "0.17.1232141"
1567+
1568+
assert extract_major_minor(version_string1) == "0.3"
1569+
assert extract_major_minor(version_string2) == "0.17"

0 commit comments

Comments
 (0)