diff --git a/.github/workflows/integration_delivery.yml b/.github/workflows/integration_delivery.yml index 3aa8802c..54cb2e70 100644 --- a/.github/workflows/integration_delivery.yml +++ b/.github/workflows/integration_delivery.yml @@ -384,14 +384,14 @@ jobs: bigger than 2GB run: | for file in artifacts/*; do - if [ $(stat -c%s "$file") -gt 2000000000 ]; then - split -b 2000000000 "$file" "$file"_ + if [ $(stat -c%s "$file") -gt 2147000000 ]; then + split -b 2147000000 "$file" "$file"_ rm "$file" fi done - name: Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: files: artifacts/* tag_name: ${{ needs.build.outputs.version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index a2d6a697..4e62cca6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Version 0.11.4 + +- feat(docker): add ngrok service (currently serves port 22 with no auth token) + ## Version 0.11.3 - test: add wireless flow test, work in progress diff --git a/poetry.lock b/poetry.lock index e1474a80..5156184e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1439,13 +1439,13 @@ typing-extensions = ">=4.10.0,<5.0.0" [[package]] name = "python-redux" -version = "0.13.1" +version = "0.13.2" description = "Redux implementation for Python" optional = false python-versions = "<4.0,>=3.11" files = [ - {file = "python_redux-0.13.1-py3-none-any.whl", hash = "sha256:93bc67c3d9148ba36de5b14ff51a1ef656cb864c9ad27700bf8d84d33cfb2503"}, - {file = "python_redux-0.13.1.tar.gz", hash = "sha256:1eeb8a196eee902c2a5d074f76cb9284e47f1185c0278e719762100bc5fc50b9"}, + {file = "python_redux-0.13.2-py3-none-any.whl", hash = "sha256:12ab0ddd9a7f074c59dd80835b92e3db8b4e39417e12c73dc91e4bd62fb99988"}, + {file = "python_redux-0.13.2.tar.gz", hash = "sha256:08b29652ac50199bad137cb2dc4f68bb2e70f0147ad2990776fad3a22465d43b"}, ] [package.dependencies] @@ -1597,14 +1597,13 @@ pyobjc-framework-Cocoa = {version = "*", markers = "sys_platform == \"darwin\""} [[package]] name = "sdbus" -version = "0.11.1" +version = "0.12.0" description = "Modern Python D-Bus library. Based on sd-bus from libsystemd." optional = false python-versions = ">=3.7" files = [ - {file = "sdbus-0.11.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89b9e2d1902d69849a62fce7fd3d65af302e9aa7cc2873e1b77499d713c21c2e"}, - {file = "sdbus-0.11.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b95cfcdb8af934f52cefed26f02ed49e2e873de5bb70f41d974c8b63dae00002"}, - {file = "sdbus-0.11.1.tar.gz", hash = "sha256:adb97718ce996bb308520682c50b1a13e606d65a6edb1c1967a15d2e570cb3b7"}, + {file = "sdbus-0.12.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d918c8ad14ef00e589d6752ac0f2b6540a20c625e85000c152376945fca14209"}, + {file = "sdbus-0.12.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d545e65637536a63898e2366b2a74617aa1a2215b1c16b23475f912e3407838c"}, ] [[package]] @@ -1845,4 +1844,4 @@ dev = ["ubo-gui", "ubo-gui"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "5af13865d72d40071a5f97bc67ec5f023571704ebc885d7ebe0416fcc1026fc3" +content-hash = "126bba20d0a49c3d196eea7a05a2f52d2f9f36d3cb07b79d30f6289829cceb10" diff --git a/pyproject.toml b/pyproject.toml index 701be9ed..9af68a1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ ubo-gui = [ 'dev', ] }, ] -python-redux = "^0.13.1" +python-redux = "^0.13.2" pyzbar = "^0.1.9" sdbus-networkmanager = { version = "^2.0.0", markers = "platform_machine=='aarch64'" } rpi_ws281x = { version = "^5.0.0", markers = "platform_machine=='aarch64'" } diff --git a/scripts/Dockerfile.dev b/scripts/Dockerfile.dev index eddc6ee4..d5c8ccc6 100644 --- a/scripts/Dockerfile.dev +++ b/scripts/Dockerfile.dev @@ -2,7 +2,7 @@ FROM ubuntu:mantic ARG DEBIAN_FRONTEND=noninteractive RUN apt -y update -RUN apt -y install curl git libcap-dev libegl1 libgl1 libmtdev1 libzbar0 python3 python3-dev +RUN apt -y install gcc curl git libcap-dev libegl1 libgl1 libmtdev1 libzbar0 python3 python3-dev RUN curl -sSL https://install.python-poetry.org | python3 - ENV PATH="${PATH}:/root/.local/bin" WORKDIR /ubo-app diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 00000000..c3373a15 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Ubo app tests.""" diff --git a/tests/conftest.py b/tests/conftest.py index fba5efcd..16499aab 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,9 @@ wait_for, ) -from tests.fixtures import ( +pytest.register_assert_rewrite('tests.fixtures') + +from tests.fixtures import ( # noqa: E402 AppContext, LoadServices, Stability, @@ -130,3 +132,15 @@ def get(self: FakeAiohttp, url: str, **kwargs: dict[str, object]) -> Fake: return parent.get(url, **kwargs) sys.modules['aiohttp'] = FakeAiohttp() + + class FakeSensor(Fake): + lux = 0.0 + temperature = 0.0 + + class FakeSensorModule(Fake): + PCT2075 = FakeSensor + VEML7700 = FakeSensor + + sys.modules['adafruit_pct2075'] = FakeSensorModule() + sys.modules['adafruit_veml7700'] = FakeSensorModule() + sys.modules['i2c'] = Fake() diff --git a/tests/fixtures/load_services.py b/tests/fixtures/load_services.py index d63292cb..3cbd112b 100644 --- a/tests/fixtures/load_services.py +++ b/tests/fixtures/load_services.py @@ -2,14 +2,30 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Callable, Sequence, TypeAlias +from typing import TYPE_CHECKING, Coroutine, Literal, Protocol, Sequence, cast, overload import pytest if TYPE_CHECKING: from tests.conftest import WaitFor -LoadServices: TypeAlias = Callable[[Sequence[str]], None] + +class LoadServices(Protocol): + """Load services and wait for them to be ready.""" + + @overload + def __call__( + self: LoadServices, + service_ids: Sequence[str], + ) -> None: ... + + @overload + def __call__( + self: LoadServices, + service_ids: Sequence[str], + *, + run_async: Literal[True], + ) -> Coroutine[None, None, None]: ... @pytest.fixture() @@ -18,12 +34,14 @@ def load_services(wait_for: WaitFor) -> LoadServices: def load_services_and_wait( service_ids: Sequence[str], - ) -> None: + *, + run_async: bool = False, + ) -> Coroutine[None, None, None] | None: from ubo_app.load_services import load_services load_services(service_ids) - @wait_for + @wait_for(run_async=cast(Literal[True], run_async)) def check() -> None: from ubo_app.load_services import REGISTERED_PATHS @@ -33,6 +51,6 @@ def check() -> None: for service in REGISTERED_PATHS.values() ), f'{service_id} not loaded' - check() + return check() - return load_services_and_wait + return cast(LoadServices, load_services_and_wait) diff --git a/tests/fixtures/snapshot.py b/tests/fixtures/snapshot.py index 5df72c5a..68cac490 100644 --- a/tests/fixtures/snapshot.py +++ b/tests/fixtures/snapshot.py @@ -55,7 +55,7 @@ def __init__( ) if self.results_dir.exists(): for file in self.results_dir.glob( - 'window:*' if override else 'window:*.mismatch.*', + 'window-*' if override else 'window-*.mismatch.*', ): file.unlink() self.results_dir.mkdir(parents=True, exist_ok=True) @@ -113,8 +113,16 @@ def take(self: WindowSnapshot, title: str | None = None) -> None: hash_mismatch_path.write_text( # pragma: no cover f'// MISMATCH: {filename}\n{new_snapshot}\n', ) - write_image(image_mismatch_path, array) - assert new_snapshot == old_snapshot, f'Window snapshot mismatch: {title}' + if self.make_screenshots: + write_image(image_mismatch_path, array) + elif self.make_screenshots: + write_image(image_path, array) + if title: + assert ( + new_snapshot == old_snapshot + ), f'Window snapshot mismatch for {title}' + else: + assert new_snapshot == old_snapshot, 'Window snapshot mismatch' self.test_counter[title] += 1 diff --git a/tests/integration/results/test_services/all_services_register/store-000.jsonc b/tests/integration/results/test_services/all_services_register/store-000.jsonc index 0006de3d..adfd3976 100644 --- a/tests/integration/results/test_services/all_services_register/store-000.jsonc +++ b/tests/integration/results/test_services/all_services_register/store-000.jsonc @@ -32,6 +32,19 @@ "ports": [], "status": "not_available" }, + "ngrok": { + "container_ip": null, + "docker_id": null, + "icon": "smart_toy", + "id": "ngrok", + "ip_addresses": [ + "192.168.1.1" + ], + "label": "Ngrok", + "path": "ngrok/ngrok:latest", + "ports": [], + "status": "not_available" + }, "ollama": { "container_ip": null, "docker_id": null, diff --git a/tests/integration/test_services.py b/tests/integration/test_services.py index 8e34f3c1..e13aaca9 100644 --- a/tests/integration/test_services.py +++ b/tests/integration/test_services.py @@ -40,5 +40,5 @@ async def test_all_services_register( app_context.set_app(app) load_services(ALL_SERVICES_LABELS) await stability() - window_snapshot.take() store_snapshot.take() + window_snapshot.take() diff --git a/ubo_app/services/040-sensors/setup.py b/ubo_app/services/040-sensors/setup.py index cdb95087..1cd4c7ac 100644 --- a/ubo_app/services/040-sensors/setup.py +++ b/ubo_app/services/040-sensors/setup.py @@ -1,19 +1,19 @@ """Setup the service.""" + from __future__ import annotations from datetime import datetime, timezone -import adafruit_pct2075 -import adafruit_veml7700 -import board -from kivy.clock import Clock - from ubo_app.store import dispatch from ubo_app.store.services.sensors import Sensor, SensorsReportReadingAction def read_sensors(_: float | None = None) -> None: """Read the sensor.""" + import adafruit_pct2075 + import adafruit_veml7700 + import board + i2c = board.I2C() temperature_sensor = adafruit_pct2075.PCT2075(i2c, address=0x48) temperature = temperature_sensor.temperature @@ -35,5 +35,7 @@ def read_sensors(_: float | None = None) -> None: def init_service() -> None: """Initialize the service.""" + from kivy.clock import Clock + Clock.schedule_interval(read_sensors, 1) read_sensors() diff --git a/ubo_app/services/080-docker/image.py b/ubo_app/services/080-docker/image.py index 9c3c3df0..7b7f94ec 100644 --- a/ubo_app/services/080-docker/image.py +++ b/ubo_app/services/080-docker/image.py @@ -25,6 +25,11 @@ ImageState, ImageStatus, ) +from ubo_app.store.services.notifications import ( + Importance, + Notification, + NotificationsAddAction, +) from ubo_app.utils.async_ import create_task, run_in_executor @@ -277,14 +282,35 @@ def act() -> None: if container.status != 'running': container.start() else: - hosts = { - key: getattr(docker_state, value).container_ip - if hasattr(docker_state, value) - else value - for key, value in IMAGES[image.id].hosts.items() - if not hasattr(docker_state, value) - or getattr(docker_state, value).container_ip - } + hosts = {} + for key, value in IMAGES[image.id].hosts.items(): + if not hasattr(docker_state, value): + dispatch( + NotificationsAddAction( + notification=Notification( + title='Dependency error', + content=f'Container "{value}" is not loaded', + importance=Importance.MEDIUM, + ), + ), + ) + return + if not getattr(docker_state, value).container_ip: + dispatch( + NotificationsAddAction( + notification=Notification( + title='Dependency error', + content=f'Container "{value}" does not have an IP' + ' address', + importance=Importance.MEDIUM, + ), + ), + ) + return + if hasattr(docker_state, value): + hosts[key] = getattr(docker_state, value).container_ip + else: + hosts[key] = value docker_client.containers.run( image.path, hostname=image.id, @@ -292,6 +318,8 @@ def act() -> None: detach=True, volumes=IMAGES[image.id].volumes, ports=IMAGES[image.id].ports, + network_mode=IMAGES[image.id].network_mode, + environment=IMAGES[image.id].environment, extra_hosts=hosts, restart_policy='always', ) diff --git a/ubo_app/services/080-docker/reducer.py b/ubo_app/services/080-docker/reducer.py index 788c3281..ddff6374 100644 --- a/ubo_app/services/080-docker/reducer.py +++ b/ubo_app/services/080-docker/reducer.py @@ -1,4 +1,5 @@ """Docker reducer.""" + from __future__ import annotations from dataclasses import field, replace @@ -54,8 +55,12 @@ class ImageEntry(Immutable): label: str icon: str path: str + dependencies: list[str] | None = None ports: dict[str, str] = field(default_factory=dict) hosts: dict[str, str] = field(default_factory=dict) + note: str | None = None + environment: dict[str, str] | None = None + network_mode: str = 'bridge' volumes: list[str] | None = None @@ -86,6 +91,8 @@ class ImageEntry(Immutable): id='pi_hole', label='Pi-hole', icon='dns', + environment={'WEBPASSWORD': 'admin'}, + note='Password: admin', path=DOCKER_PREFIX + 'pihole/pihole:latest', ), ImageEntry( @@ -100,9 +107,19 @@ class ImageEntry(Immutable): label='Open WebUI', icon='code', path=DOCKER_PREFIX + 'ghcr.io/open-webui/open-webui:main', + dependencies=['ollama'], + network_mode='container:ollama', ports={'8080/tcp': '8080'}, hosts={'host.docker.internal': 'ollama'}, ), + ImageEntry( + id='ngrok', + label='Ngrok', + icon='smart_toy', + network_mode='host', + path=DOCKER_PREFIX + 'ngrok/ngrok:latest', + ports={'22/tcp': '22'}, + ), *( [ ImageEntry(