Skip to content
Merged
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
41 changes: 41 additions & 0 deletions packages/cli/src/lablink_cli/commands/doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,44 @@ def _check_default_client_ami(region: str, published: str | None) -> dict:
return result


def _check_viewer_streaming(cfg) -> dict:
"""Warn when the viewer will be served over plain HTTP.

KasmVNC's H.264 path needs the browser's WebCodecs VideoDecoder, which Chrome
exposes only on secure origins. With ssl.provider "none" the viewer is served
over http://, the codec probe reports "WebCodecs API not available", and every
session falls back to JPEG/WebP stills — re-encoding damaged regions from
scratch every frame instead of encoding inter-frame change.

Nothing else surfaces this. The deploy is green, sessions work, and the
server-side encoder is fine; it simply never gets asked for video mode. The
symptom reaches an operator as "the desktop feels laggy", which is a long way
from "ssl.provider is none".
"""
result = {"check": "Viewer streaming", "status": "pass"}

if cfg is None:
result["status"] = "warn"
result["detail"] = "Skipped (no valid config)"
return result

provider = (getattr(getattr(cfg, "ssl", None), "provider", "") or "none").lower()

if provider == "none":
result["status"] = "warn"
result["detail"] = (
"ssl.provider is 'none', so the viewer is served over HTTP and H.264 "
"streaming cannot engage — sessions fall back to JPEG/WebP. Configure an "
"SSL provider, or to test as-is port-forward the allocator "
"(ssh -L 8443:localhost:5000 ...) and open http://localhost:8443, since "
"localhost counts as a secure origin"
)
return result

result["detail"] = f"H.264 available — viewer served over HTTPS ({provider})"
return result


def _check_ami(cfg) -> dict:
"""Check that the configured client AMI exists in the configured region.

Expand Down Expand Up @@ -378,6 +416,9 @@ def _check_aws_prereqs() -> None:
# 6. Client AMI
checks.append(_check_ami(cfg))

# 7. Viewer streaming
checks.append(_check_viewer_streaming(cfg))

_render_checks(
checks,
pass_message=(
Expand Down
42 changes: 42 additions & 0 deletions packages/cli/tests/test_doctor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
import json
from unittest.mock import MagicMock, patch

import pytest


from lablink_cli.commands.doctor import (
_check_viewer_streaming,
_check_ami,
_check_opentofu,
)
Expand Down Expand Up @@ -438,3 +441,42 @@ def test_renders_all_three_checks(self, capsys):
assert "Registered" in out
assert "Log shipper" in out
assert "All checks passed" in out


# ------------------------------------------------------------------
# _check_viewer_streaming
# ------------------------------------------------------------------
def _ssl_cfg(provider):
cfg = MagicMock()
cfg.ssl.provider = provider
return cfg


class TestCheckViewerStreaming:
def test_no_config(self):
result = _check_viewer_streaming(None)
assert result["status"] == "warn"

def test_http_deployment_warns_with_a_way_to_test(self):
"""The whole point: an HTTP deployment silently loses H.264, and the
operator sees 'laggy desktop', not 'ssl.provider is none'."""
result = _check_viewer_streaming(_ssl_cfg("none"))
assert result["status"] == "warn"
assert "JPEG/WebP" in result["detail"]
assert "localhost" in result["detail"], "must name the port-forward workaround"

def test_warns_rather_than_fails(self):
"""HTTP is a supported deployment, just a slower one — failing preflight
over a performance cliff would block a legitimate config."""
assert _check_viewer_streaming(_ssl_cfg("none"))["status"] != "fail"

@pytest.mark.parametrize("provider", ["letsencrypt", "cloudflare", "acm"])
def test_https_providers_pass(self, provider):
result = _check_viewer_streaming(_ssl_cfg(provider))
assert result["status"] == "pass"
assert provider in result["detail"]

def test_missing_ssl_section_is_treated_as_http(self):
cfg = MagicMock()
cfg.ssl.provider = None
assert _check_viewer_streaming(cfg)["status"] == "warn"
Loading