From 597bb6c9d9b6d5c951eea2cb1595bcac325a36aa Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 21 May 2026 13:47:28 +0400 Subject: [PATCH] 0.3.4: validate --deep, fix stale docker-compose.yaml validate --deep pings Open WebUI API to verify connectivity, API key validity, and that each KB ID exists. Catches config errors before deployment. Also applies defaults and env var interpolation in validate (was missing). docker-compose.yaml updated from stale watch mode to production daemon mode with healthcheck, LOG_FORMAT, and OIKB_API_KEY. --- CHANGELOG.md | 11 ++++++++++ docker-compose.yaml | 13 ++++++++--- docs/guide.md | 4 +++- pyproject.toml | 2 +- src/oikb/__init__.py | 2 +- src/oikb/cli.py | 51 ++++++++++++++++++++++++++++++++++++++++---- src/oikb/daemon.py | 2 +- 7 files changed, 74 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f73f623..348f73d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [0.3.4] - 2025-05-21 + +### Added + +- `oikb validate --deep` — verifies Open WebUI connectivity, API key validity, and that each KB ID exists. Catches config errors before deployment. + +### Fixed + +- `docker-compose.yaml` updated from stale `watch` mode to production `daemon` mode with healthcheck, `LOG_FORMAT=json`, and `OIKB_API_KEY` support. +- `validate` now applies `defaults:` and env var interpolation to entries before checking (was only doing raw yaml). + ## [0.3.3] - 2025-05-21 ### Added diff --git a/docker-compose.yaml b/docker-compose.yaml index 95107a2..bd56269 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -6,18 +6,25 @@ services: volumes: - open-webui:/app/backend/data - # One-shot sync on container start, then watch for changes oikb: image: ghcr.io/open-webui/oikb:latest environment: - OPEN_WEBUI_URL=http://open-webui:8080 - OPEN_WEBUI_API_KEY=${OPEN_WEBUI_API_KEY} + - OIKB_API_KEY=${OIKB_API_KEY} + - LOG_FORMAT=json volumes: - - ./docs:/data - command: watch /data --kb ${KB_ID} + - ./.oikb.yaml:/app/.oikb.yaml:ro + command: daemon + ports: + - "8080:8080" depends_on: - open-webui restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health/ready"] + interval: 30s + timeout: 5s volumes: open-webui: diff --git a/docs/guide.md b/docs/guide.md index ba871fe..e81010e 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -645,6 +645,7 @@ oikb daemon --log-format json JSON logging oikb daemon --config /path/to/yaml Custom config path oikb diff --kb-id ID Preview changes oikb validate Check .oikb.yaml syntax +oikb validate --deep Verify API, auth, and KB existence oikb ls --kb-id ID List files in a KB oikb status --kb-id ID Show KB info oikb history View sync history @@ -686,7 +687,8 @@ You're running `oikb sync` without arguments and there's no `.oikb.yaml` in the ### Daemon won't start ```bash -oikb validate # Check config syntax first +oikb validate # Check config syntax +oikb validate --deep # Verify API + KB connectivity ``` ### How do I find my KB ID? diff --git a/pyproject.toml b/pyproject.toml index 8545b5e..a09710d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "oikb" -version = "0.3.3" +version = "0.3.4" description = "Sync anything to Open WebUI Knowledge Bases" readme = "README.md" authors = [ diff --git a/src/oikb/__init__.py b/src/oikb/__init__.py index 0579874..82e68b1 100644 --- a/src/oikb/__init__.py +++ b/src/oikb/__init__.py @@ -1,3 +1,3 @@ """oikb — Open WebUI Knowledge Base CLI.""" -__version__ = "0.3.3" +__version__ = "0.3.4" diff --git a/src/oikb/cli.py b/src/oikb/cli.py index aaba9d9..b7bffbc 100644 --- a/src/oikb/cli.py +++ b/src/oikb/cli.py @@ -830,13 +830,23 @@ def init(force: bool): @cli.command() @click.option("--config", "config_file", default=None, type=click.Path(), help="Path to .oikb.yaml (default: ./.oikb.yaml).") -def validate(config_file: str | None): - """Validate .oikb.yaml without running anything.""" +@click.option("--deep", is_flag=True, help="Verify connectivity: ping Open WebUI, check API key, confirm each KB exists.") +def validate(config_file: str | None, deep: bool): + """Validate .oikb.yaml without running anything. + + By default, checks YAML structure and source syntax. + With --deep, also verifies the Open WebUI API is reachable, + the API key is valid, and each Knowledge Base ID exists. + """ if config_file: import yaml with open(config_file) as f: data = yaml.safe_load(f) + data = _interpolate_env(data) if data else data entries = (data.get("sources") or data.get("sync", [])) if data else [] + defaults = data.get("defaults", {}) if data else {} + if defaults and entries: + entries = [_deep_merge(defaults, e) for e in entries] else: entries = _load_oikb_yaml() @@ -844,6 +854,20 @@ def validate(config_file: str | None): click.echo(click.style("No .oikb.yaml found or file is empty.", fg="red"), err=True) sys.exit(1) + # Deep validation: create a client and verify connectivity first. + client = None + if deep: + click.echo(click.style("Deep validation\n", bold=True)) + try: + client = _make_client( + url=entries[0].get("url"), + token=entries[0].get("token"), + ) + click.echo(click.style(" ✓ API reachable", fg="green") + f" {client._http.base_url}") + except Exception as e: + click.echo(click.style(f" ✗ Cannot reach Open WebUI: {e}", fg="red")) + sys.exit(1) + has_errors = False for i, entry in enumerate(entries, 1): entry_name = entry.get("name", f"entry #{i}") @@ -855,13 +879,32 @@ def validate(config_file: str | None): has_errors = True continue - # Try resolving the connector to catch bad source strings. + # Syntax check: resolve the connector. try: _resolve_connector(source) - click.echo(click.style(f" ✓ {entry_name}", fg="green") + f" {source} → {kb_id}") except Exception as e: click.echo(click.style(f" ✗ {entry_name}: {e}", fg="red")) has_errors = True + continue + + # Deep check: verify the KB exists. + if deep and client: + try: + kb = client.get_kb(kb_id) + kb_name = kb.get("name", "?") + file_count = len(kb.get("files", [])) + click.echo( + click.style(f" ✓ {entry_name}", fg="green") + + f" {source} → {kb_name} ({file_count} files)" + ) + except Exception as e: + click.echo(click.style(f" ✗ {entry_name}: KB {kb_id} — {e}", fg="red")) + has_errors = True + else: + click.echo(click.style(f" ✓ {entry_name}", fg="green") + f" {source} → {kb_id}") + + if client: + client.close() if has_errors: sys.exit(1) diff --git a/src/oikb/daemon.py b/src/oikb/daemon.py index 9879c54..e9cf391 100644 --- a/src/oikb/daemon.py +++ b/src/oikb/daemon.py @@ -40,7 +40,7 @@ async def verify_api_key( app = FastAPI( title="oikb", description="Sync engine for Open WebUI Knowledge Bases. Trigger syncs, check status, and query history.", - version="0.3.3", + version="0.3.4", ) # Runtime state populated by start_daemon().