Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dev/breeze/src/airflow_breeze/global_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,7 @@ def get_default_platform_machine() -> str:
REDIS_HOST_PORT = "26379"
RABBITMQ_HOST_PORT = "25672"
SSH_PORT = "12322"
SIMPLE_AUTH_MANAGER_VITE_DEV_PORT = "5174"
VITE_DEV_PORT = "5173"
WEB_HOST_PORT = "28080"
BREEZE_DEBUG_SCHEDULER_PORT = "50231"
Expand Down
22 changes: 21 additions & 1 deletion dev/breeze/src/airflow_breeze/utils/run_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,18 @@
import shlex
import shutil
import signal
import socket
import stat
import subprocess
import sys
from collections.abc import Mapping
from collections.abc import Iterable, Mapping
from pathlib import Path
from subprocess import CalledProcessError, CompletedProcess
from typing import Any

from rich.markup import escape

from airflow_breeze.global_constants import SIMPLE_AUTH_MANAGER_VITE_DEV_PORT, VITE_DEV_PORT
from airflow_breeze.utils.ci_group import ci_group
from airflow_breeze.utils.console import Output, console_print, get_console
from airflow_breeze.utils.functools_cache import clearable_cache
Expand Down Expand Up @@ -532,12 +534,30 @@ def _clean_ui_assets(additional_ui_hooks: list[str]):
console_print("[success]Cleaned ui assets[/]")


def _find_occupied_local_ports(ports: Iterable[str]) -> list[str]:
occupied_ports = []
for port in ports:
with contextlib.suppress(OSError):
with socket.create_connection(("localhost", int(port)), timeout=0.1):
occupied_ports.append(port)
return occupied_ports


def run_compile_ui_assets(
dev: bool,
run_in_background: bool,
force_clean: bool,
additional_ui_hooks: list[str],
):
if dev:
occupied_ports = _find_occupied_local_ports((VITE_DEV_PORT, SIMPLE_AUTH_MANAGER_VITE_DEV_PORT))
if occupied_ports:
console_print(
"[error]Cannot start UI development servers because the following local port(s) "
f"are already in use: {', '.join(occupied_ports)}.[/]\n"
"[info]Stop the processes using these ports and try again.[/]"
)
sys.exit(1)
if force_clean:
_clean_ui_assets(additional_ui_hooks)
if dev:
Expand Down
68 changes: 68 additions & 0 deletions dev/breeze/tests/test_run_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,20 @@
# under the License.
from __future__ import annotations

import socket
import stat
from unittest import mock

import pytest

from airflow_breeze.global_constants import SIMPLE_AUTH_MANAGER_VITE_DEV_PORT, VITE_DEV_PORT
from airflow_breeze.utils.run_utils import (
_find_occupied_local_ports,
change_directory_permission,
change_file_permission,
check_if_buildx_plugin_installed,
run_command,
run_compile_ui_assets,
)


Expand Down Expand Up @@ -57,6 +63,68 @@ def test_run_command_dry_run_quiet_does_not_execute(mock_subprocess_run):
assert result.stderr == ""


def test_find_occupied_local_ports():
with socket.socket() as unused_socket:
unused_socket.bind(("127.0.0.1", 0))
unoccupied_port = str(unused_socket.getsockname()[1])

with socket.create_server(("127.0.0.1", 0)) as server:
occupied_port = str(server.getsockname()[1])

assert _find_occupied_local_ports((unoccupied_port, occupied_port)) == [occupied_port]


@mock.patch("airflow_breeze.utils.run_utils._run_compile_internally")
@mock.patch("airflow_breeze.utils.run_utils._find_occupied_local_ports", return_value=[])
def test_run_compile_ui_assets_checks_both_dev_ports(mock_find_occupied_ports, mock_run_compile):
result = run_compile_ui_assets(
dev=True, run_in_background=False, force_clean=False, additional_ui_hooks=[]
)

assert result == mock_run_compile.return_value
mock_find_occupied_ports.assert_called_once_with((VITE_DEV_PORT, SIMPLE_AUTH_MANAGER_VITE_DEV_PORT))


@pytest.mark.parametrize(
"occupied_ports",
[
pytest.param([VITE_DEV_PORT], id="airflow-ui"),
pytest.param([SIMPLE_AUTH_MANAGER_VITE_DEV_PORT], id="simple-auth-manager-ui"),
],
)
@mock.patch("airflow_breeze.utils.run_utils._clean_ui_assets")
@mock.patch("airflow_breeze.utils.run_utils._find_occupied_local_ports")
@mock.patch("airflow_breeze.utils.run_utils.console_print")
def test_run_compile_ui_assets_exits_before_cleanup_when_dev_port_is_occupied(
mock_console_print, mock_find_occupied_ports, mock_clean_ui_assets, occupied_ports
):
mock_find_occupied_ports.return_value = occupied_ports

with pytest.raises(SystemExit) as ctx:
run_compile_ui_assets(dev=True, run_in_background=False, force_clean=True, additional_ui_hooks=[])

assert ctx.value.code == 1
mock_console_print.assert_called_once_with(
"[error]Cannot start UI development servers because the following local port(s) "
f"are already in use: {', '.join(occupied_ports)}.[/]\n"
"[info]Stop the processes using these ports and try again.[/]"
)
mock_clean_ui_assets.assert_not_called()


@mock.patch("airflow_breeze.utils.run_utils._run_compile_internally")
@mock.patch("airflow_breeze.utils.run_utils._find_occupied_local_ports")
def test_run_compile_ui_assets_does_not_check_dev_ports_for_static_build(
mock_find_occupied_ports, mock_run_compile
):
result = run_compile_ui_assets(
dev=False, run_in_background=False, force_clean=False, additional_ui_hooks=[]
)

assert result == mock_run_compile.return_value
mock_find_occupied_ports.assert_not_called()


@mock.patch("airflow_breeze.utils.run_utils.run_command")
@mock.patch("airflow_breeze.utils.run_utils.console_print")
def test_check_buildah_is_installed(mock_console_print, mock_run_command):
Expand Down
Loading