Wire all 25 services into --randomize-services and add heterogeneous OS fingerprints
- Replace hardcoded ALL_SERVICE_NAMES=[5 services] in cli.py with
_all_service_names() pulling dynamically from the plugin registry;
randomize-services now draws from all 25 registered honeypots
- Add build_base field to DistroProfile: apt-compatible image for service
Dockerfiles (ubuntu22/ubuntu20/kali get their own; others fall back to
debian:bookworm-slim since Dockerfiles use apt-get)
- Add build_base to DeckyConfig; propagate from distro in _build_deckies
and _build_deckies_from_ini
- Inject BASE_IMAGE build arg in composer.py for every build-based service
so each decky's containers reflect its assigned distro
- Update all 21 service Dockerfiles: FROM debian:bookworm-slim →
ARG BASE_IMAGE=debian:bookworm-slim / FROM ${BASE_IMAGE}
- Add tests/test_cli_service_pool.py and tests/test_composer.py (306 total)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,9 @@ app = typer.Typer(
|
|||||||
)
|
)
|
||||||
console = Console()
|
console = Console()
|
||||||
|
|
||||||
ALL_SERVICE_NAMES = ["ssh", "smb", "rdp", "http", "ftp"]
|
def _all_service_names() -> list[str]:
|
||||||
|
"""Return all registered service names from the live plugin registry."""
|
||||||
|
return sorted(all_services().keys())
|
||||||
|
|
||||||
|
|
||||||
def _resolve_distros(
|
def _resolve_distros(
|
||||||
@@ -71,11 +73,12 @@ def _build_deckies(
|
|||||||
if services_explicit:
|
if services_explicit:
|
||||||
svc_list = services_explicit
|
svc_list = services_explicit
|
||||||
elif randomize_services:
|
elif randomize_services:
|
||||||
# Pick 1-3 random services, try to avoid exact duplicates
|
# Pick 1-3 random services from the full registry, avoid exact duplicates
|
||||||
|
svc_pool = _all_service_names()
|
||||||
attempts = 0
|
attempts = 0
|
||||||
while True:
|
while True:
|
||||||
count = random.randint(1, min(3, len(ALL_SERVICE_NAMES)))
|
count = random.randint(1, min(3, len(svc_pool)))
|
||||||
chosen = frozenset(random.sample(ALL_SERVICE_NAMES, count))
|
chosen = frozenset(random.sample(svc_pool, count))
|
||||||
attempts += 1
|
attempts += 1
|
||||||
if chosen not in used_combos or attempts > 20:
|
if chosen not in used_combos or attempts > 20:
|
||||||
break
|
break
|
||||||
@@ -92,6 +95,7 @@ def _build_deckies(
|
|||||||
services=svc_list,
|
services=svc_list,
|
||||||
distro=distro.slug,
|
distro=distro.slug,
|
||||||
base_image=distro.image,
|
base_image=distro.image,
|
||||||
|
build_base=distro.build_base,
|
||||||
hostname=hostname,
|
hostname=hostname,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -136,18 +140,19 @@ def _build_deckies_from_ini(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if spec.services:
|
if spec.services:
|
||||||
known = set(ALL_SERVICE_NAMES)
|
known = set(_all_service_names())
|
||||||
unknown = [s for s in spec.services if s not in known]
|
unknown = [s for s in spec.services if s not in known]
|
||||||
if unknown:
|
if unknown:
|
||||||
console.print(
|
console.print(
|
||||||
f"[red]Unknown service(s) in [{spec.name}]: {unknown}. "
|
f"[red]Unknown service(s) in [{spec.name}]: {unknown}. "
|
||||||
f"Available: {ALL_SERVICE_NAMES}[/]"
|
f"Available: {_all_service_names()}[/]"
|
||||||
)
|
)
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
svc_list = spec.services
|
svc_list = spec.services
|
||||||
elif randomize:
|
elif randomize:
|
||||||
count = random.randint(1, min(3, len(ALL_SERVICE_NAMES)))
|
svc_pool = _all_service_names()
|
||||||
svc_list = random.sample(ALL_SERVICE_NAMES, count)
|
count = random.randint(1, min(3, len(svc_pool)))
|
||||||
|
svc_list = random.sample(svc_pool, count)
|
||||||
else:
|
else:
|
||||||
console.print(
|
console.print(
|
||||||
f"[red]Decky '[{spec.name}]' has no services= in config. "
|
f"[red]Decky '[{spec.name}]' has no services= in config. "
|
||||||
@@ -161,6 +166,7 @@ def _build_deckies_from_ini(
|
|||||||
services=svc_list,
|
services=svc_list,
|
||||||
distro=distro.slug,
|
distro=distro.slug,
|
||||||
base_image=distro.image,
|
base_image=distro.image,
|
||||||
|
build_base=distro.build_base,
|
||||||
hostname=hostname,
|
hostname=hostname,
|
||||||
))
|
))
|
||||||
return deckies
|
return deckies
|
||||||
@@ -225,10 +231,10 @@ def deploy(
|
|||||||
|
|
||||||
services_list = [s.strip() for s in services.split(",")] if services else None
|
services_list = [s.strip() for s in services.split(",")] if services else None
|
||||||
if services_list:
|
if services_list:
|
||||||
known = set(ALL_SERVICE_NAMES)
|
known = set(_all_service_names())
|
||||||
unknown = [s for s in services_list if s not in known]
|
unknown = [s for s in services_list if s not in known]
|
||||||
if unknown:
|
if unknown:
|
||||||
console.print(f"[red]Unknown service(s): {unknown}. Available: {ALL_SERVICE_NAMES}[/]")
|
console.print(f"[red]Unknown service(s): {unknown}. Available: {_all_service_names()}[/]")
|
||||||
raise typer.Exit(1)
|
raise typer.Exit(1)
|
||||||
|
|
||||||
if not services_list and not randomize_services:
|
if not services_list and not randomize_services:
|
||||||
|
|||||||
@@ -48,6 +48,11 @@ def generate_compose(config: DecnetConfig) -> dict:
|
|||||||
svc = get_service(svc_name)
|
svc = get_service(svc_name)
|
||||||
fragment = svc.compose_fragment(decky.name, log_target=config.log_target)
|
fragment = svc.compose_fragment(decky.name, log_target=config.log_target)
|
||||||
|
|
||||||
|
# Inject the per-decky base image into build services so containers
|
||||||
|
# vary by distro and don't all fingerprint as debian:bookworm-slim.
|
||||||
|
if "build" in fragment:
|
||||||
|
fragment["build"].setdefault("args", {})["BASE_IMAGE"] = decky.build_base
|
||||||
|
|
||||||
fragment.setdefault("environment", {})
|
fragment.setdefault("environment", {})
|
||||||
fragment["environment"]["HOSTNAME"] = decky.hostname
|
fragment["environment"]["HOSTNAME"] = decky.hostname
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ class DeckyConfig(BaseModel):
|
|||||||
ip: str
|
ip: str
|
||||||
services: list[str]
|
services: list[str]
|
||||||
distro: str # slug from distros.DISTROS, e.g. "debian", "ubuntu22"
|
distro: str # slug from distros.DISTROS, e.g. "debian", "ubuntu22"
|
||||||
base_image: str # resolved Docker image tag
|
base_image: str # Docker image for the base/IP-holder container
|
||||||
|
build_base: str = "debian:bookworm-slim" # apt-compatible image for service Dockerfiles
|
||||||
hostname: str
|
hostname: str
|
||||||
|
|
||||||
@field_validator("services")
|
@field_validator("services")
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ from dataclasses import dataclass
|
|||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class DistroProfile:
|
class DistroProfile:
|
||||||
slug: str # CLI-facing identifier, e.g. "debian", "rocky9"
|
slug: str # CLI-facing identifier, e.g. "debian", "rocky9"
|
||||||
image: str # Docker image tag
|
image: str # Docker image tag (used for the base/IP-holder container)
|
||||||
display_name: str # Human-readable label shown in tables
|
display_name: str # Human-readable label shown in tables
|
||||||
hostname_style: str # "generic" | "rhel" | "minimal" | "rolling"
|
hostname_style: str # "generic" | "rhel" | "minimal" | "rolling"
|
||||||
|
build_base: str # apt-compatible image for service Dockerfiles (FROM ${BASE_IMAGE})
|
||||||
|
|
||||||
|
|
||||||
DISTROS: dict[str, DistroProfile] = {
|
DISTROS: dict[str, DistroProfile] = {
|
||||||
@@ -23,54 +24,63 @@ DISTROS: dict[str, DistroProfile] = {
|
|||||||
image="debian:bookworm-slim",
|
image="debian:bookworm-slim",
|
||||||
display_name="Debian 12 (Bookworm)",
|
display_name="Debian 12 (Bookworm)",
|
||||||
hostname_style="generic",
|
hostname_style="generic",
|
||||||
|
build_base="debian:bookworm-slim",
|
||||||
),
|
),
|
||||||
"ubuntu22": DistroProfile(
|
"ubuntu22": DistroProfile(
|
||||||
slug="ubuntu22",
|
slug="ubuntu22",
|
||||||
image="ubuntu:22.04",
|
image="ubuntu:22.04",
|
||||||
display_name="Ubuntu 22.04 LTS (Jammy)",
|
display_name="Ubuntu 22.04 LTS (Jammy)",
|
||||||
hostname_style="generic",
|
hostname_style="generic",
|
||||||
|
build_base="ubuntu:22.04",
|
||||||
),
|
),
|
||||||
"ubuntu20": DistroProfile(
|
"ubuntu20": DistroProfile(
|
||||||
slug="ubuntu20",
|
slug="ubuntu20",
|
||||||
image="ubuntu:20.04",
|
image="ubuntu:20.04",
|
||||||
display_name="Ubuntu 20.04 LTS (Focal)",
|
display_name="Ubuntu 20.04 LTS (Focal)",
|
||||||
hostname_style="generic",
|
hostname_style="generic",
|
||||||
|
build_base="ubuntu:20.04",
|
||||||
),
|
),
|
||||||
"rocky9": DistroProfile(
|
"rocky9": DistroProfile(
|
||||||
slug="rocky9",
|
slug="rocky9",
|
||||||
image="rockylinux:9-minimal",
|
image="rockylinux:9-minimal",
|
||||||
display_name="Rocky Linux 9",
|
display_name="Rocky Linux 9",
|
||||||
hostname_style="rhel",
|
hostname_style="rhel",
|
||||||
|
build_base="debian:bookworm-slim", # Dockerfiles use apt-get; fall back to debian
|
||||||
),
|
),
|
||||||
"centos7": DistroProfile(
|
"centos7": DistroProfile(
|
||||||
slug="centos7",
|
slug="centos7",
|
||||||
image="centos:7",
|
image="centos:7",
|
||||||
display_name="CentOS 7",
|
display_name="CentOS 7",
|
||||||
hostname_style="rhel",
|
hostname_style="rhel",
|
||||||
|
build_base="debian:bookworm-slim", # Dockerfiles use apt-get; fall back to debian
|
||||||
),
|
),
|
||||||
"alpine": DistroProfile(
|
"alpine": DistroProfile(
|
||||||
slug="alpine",
|
slug="alpine",
|
||||||
image="alpine:3.19",
|
image="alpine:3.19",
|
||||||
display_name="Alpine Linux 3.19",
|
display_name="Alpine Linux 3.19",
|
||||||
hostname_style="minimal",
|
hostname_style="minimal",
|
||||||
|
build_base="debian:bookworm-slim", # Dockerfiles use apt-get; fall back to debian
|
||||||
),
|
),
|
||||||
"fedora": DistroProfile(
|
"fedora": DistroProfile(
|
||||||
slug="fedora",
|
slug="fedora",
|
||||||
image="fedora:39",
|
image="fedora:39",
|
||||||
display_name="Fedora 39",
|
display_name="Fedora 39",
|
||||||
hostname_style="rhel",
|
hostname_style="rhel",
|
||||||
|
build_base="debian:bookworm-slim", # Dockerfiles use apt-get; fall back to debian
|
||||||
),
|
),
|
||||||
"kali": DistroProfile(
|
"kali": DistroProfile(
|
||||||
slug="kali",
|
slug="kali",
|
||||||
image="kalilinux/kali-rolling",
|
image="kalilinux/kali-rolling",
|
||||||
display_name="Kali Linux (Rolling)",
|
display_name="Kali Linux (Rolling)",
|
||||||
hostname_style="rolling",
|
hostname_style="rolling",
|
||||||
|
build_base="kalilinux/kali-rolling", # Debian-based, apt-get compatible
|
||||||
),
|
),
|
||||||
"arch": DistroProfile(
|
"arch": DistroProfile(
|
||||||
slug="arch",
|
slug="arch",
|
||||||
image="archlinux:latest",
|
image="archlinux:latest",
|
||||||
display_name="Arch Linux",
|
display_name="Arch Linux",
|
||||||
hostname_style="rolling",
|
hostname_style="rolling",
|
||||||
|
build_base="debian:bookworm-slim", # Dockerfiles use apt-get; fall back to debian
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 python3-pip python3-venv \
|
python3 python3-pip python3-venv \
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 python3-pip \
|
python3 python3-pip \
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 python3-pip \
|
python3 python3-pip \
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 python3-pip \
|
python3 python3-pip \
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 \
|
python3 \
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 python3-pip \
|
python3 python3-pip \
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 \
|
python3 \
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 \
|
python3 \
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 \
|
python3 \
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 \
|
python3 \
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 \
|
python3 \
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 \
|
python3 \
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 \
|
python3 \
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 \
|
python3 \
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 python3-pip \
|
python3 python3-pip \
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 \
|
python3 \
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 \
|
python3 \
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 python3-pip \
|
python3 python3-pip \
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 \
|
python3 \
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 \
|
python3 \
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
FROM debian:bookworm-slim
|
ARG BASE_IMAGE=debian:bookworm-slim
|
||||||
|
FROM ${BASE_IMAGE}
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
python3 \
|
python3 \
|
||||||
|
|||||||
81
tests/test_cli_service_pool.py
Normal file
81
tests/test_cli_service_pool.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
"""
|
||||||
|
Tests for the CLI service pool — verifies that --randomize-services draws
|
||||||
|
from all registered services, not just the original hardcoded 5.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from decnet.cli import _all_service_names, _build_deckies
|
||||||
|
from decnet.services.registry import all_services
|
||||||
|
|
||||||
|
|
||||||
|
ORIGINAL_5 = {"ssh", "smb", "rdp", "http", "ftp"}
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_service_names_covers_full_registry():
|
||||||
|
"""_all_service_names() must return every service in the registry."""
|
||||||
|
pool = set(_all_service_names())
|
||||||
|
registry = set(all_services().keys())
|
||||||
|
assert pool == registry
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_service_names_is_sorted():
|
||||||
|
names = _all_service_names()
|
||||||
|
assert names == sorted(names)
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_service_names_includes_at_least_25():
|
||||||
|
assert len(_all_service_names()) >= 25
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_service_names_includes_all_original_5():
|
||||||
|
pool = set(_all_service_names())
|
||||||
|
assert ORIGINAL_5.issubset(pool)
|
||||||
|
|
||||||
|
|
||||||
|
def test_randomize_services_pool_exceeds_original_5():
|
||||||
|
"""
|
||||||
|
After enough random draws, at least one service outside the original 5 must appear.
|
||||||
|
With 25 services and picking 1-3 at a time, 200 draws makes this ~100% certain.
|
||||||
|
"""
|
||||||
|
all_drawn: set[str] = set()
|
||||||
|
for _ in range(200):
|
||||||
|
deckies = _build_deckies(
|
||||||
|
n=1,
|
||||||
|
ips=["10.0.0.10"],
|
||||||
|
services_explicit=None,
|
||||||
|
randomize_services=True,
|
||||||
|
)
|
||||||
|
all_drawn.update(deckies[0].services)
|
||||||
|
|
||||||
|
beyond_original = all_drawn - ORIGINAL_5
|
||||||
|
assert beyond_original, (
|
||||||
|
f"After 200 draws only saw the original 5 services. "
|
||||||
|
f"All drawn: {sorted(all_drawn)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_deckies_randomize_services_valid():
|
||||||
|
"""All randomly chosen services must exist in the registry."""
|
||||||
|
registry = set(all_services().keys())
|
||||||
|
for _ in range(50):
|
||||||
|
deckies = _build_deckies(
|
||||||
|
n=3,
|
||||||
|
ips=["10.0.0.10", "10.0.0.11", "10.0.0.12"],
|
||||||
|
services_explicit=None,
|
||||||
|
randomize_services=True,
|
||||||
|
)
|
||||||
|
for decky in deckies:
|
||||||
|
unknown = set(decky.services) - registry
|
||||||
|
assert not unknown, f"Decky {decky.name} got unknown services: {unknown}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_build_deckies_explicit_services_unchanged():
|
||||||
|
"""Explicit service list must pass through untouched."""
|
||||||
|
deckies = _build_deckies(
|
||||||
|
n=2,
|
||||||
|
ips=["10.0.0.10", "10.0.0.11"],
|
||||||
|
services_explicit=["ssh", "ftp"],
|
||||||
|
randomize_services=False,
|
||||||
|
)
|
||||||
|
for decky in deckies:
|
||||||
|
assert decky.services == ["ssh", "ftp"]
|
||||||
163
tests/test_composer.py
Normal file
163
tests/test_composer.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
"""
|
||||||
|
Tests for the composer — verifies BASE_IMAGE injection and distro heterogeneity.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from decnet.config import DeckyConfig, DecnetConfig
|
||||||
|
from decnet.composer import generate_compose
|
||||||
|
from decnet.distros import all_distros, DISTROS
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
APT_COMPATIBLE = {
|
||||||
|
"debian:bookworm-slim",
|
||||||
|
"ubuntu:22.04",
|
||||||
|
"ubuntu:20.04",
|
||||||
|
"kalilinux/kali-rolling",
|
||||||
|
}
|
||||||
|
|
||||||
|
BUILD_SERVICES = [
|
||||||
|
"http", "rdp", "smb", "ftp", "pop3", "imap",
|
||||||
|
"mysql", "mssql", "redis", "mongodb", "postgres",
|
||||||
|
"ldap", "vnc", "docker_api", "k8s", "sip",
|
||||||
|
"mqtt", "llmnr", "snmp", "tftp",
|
||||||
|
]
|
||||||
|
|
||||||
|
UPSTREAM_SERVICES = ["ssh", "telnet", "smtp", "elasticsearch", "conpot"]
|
||||||
|
|
||||||
|
|
||||||
|
def _make_config(services, distro="debian", base_image=None, build_base=None):
|
||||||
|
profile = DISTROS[distro]
|
||||||
|
decky = DeckyConfig(
|
||||||
|
name="decky-01",
|
||||||
|
ip="10.0.0.10",
|
||||||
|
services=services,
|
||||||
|
distro=distro,
|
||||||
|
base_image=base_image or profile.image,
|
||||||
|
build_base=build_base or profile.build_base,
|
||||||
|
hostname="test-host",
|
||||||
|
)
|
||||||
|
return DecnetConfig(
|
||||||
|
mode="unihost",
|
||||||
|
interface="eth0",
|
||||||
|
subnet="10.0.0.0/24",
|
||||||
|
gateway="10.0.0.1",
|
||||||
|
deckies=[decky],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# BASE_IMAGE injection — build services
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("svc", BUILD_SERVICES)
|
||||||
|
def test_build_service_gets_base_image_arg(svc):
|
||||||
|
"""Every build service must have BASE_IMAGE injected in compose args."""
|
||||||
|
config = _make_config([svc], distro="debian")
|
||||||
|
compose = generate_compose(config)
|
||||||
|
key = f"decky-01-{svc}"
|
||||||
|
fragment = compose["services"][key]
|
||||||
|
assert "build" in fragment, f"{svc}: missing 'build' key"
|
||||||
|
assert "args" in fragment["build"], f"{svc}: build section missing 'args'"
|
||||||
|
assert "BASE_IMAGE" in fragment["build"]["args"], f"{svc}: BASE_IMAGE not in args"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("distro,expected_build_base", [
|
||||||
|
("debian", "debian:bookworm-slim"),
|
||||||
|
("ubuntu22", "ubuntu:22.04"),
|
||||||
|
("ubuntu20", "ubuntu:20.04"),
|
||||||
|
("kali", "kalilinux/kali-rolling"),
|
||||||
|
("rocky9", "debian:bookworm-slim"),
|
||||||
|
("alpine", "debian:bookworm-slim"),
|
||||||
|
])
|
||||||
|
def test_build_service_base_image_matches_distro(distro, expected_build_base):
|
||||||
|
"""BASE_IMAGE arg must match the distro's build_base."""
|
||||||
|
config = _make_config(["http"], distro=distro)
|
||||||
|
compose = generate_compose(config)
|
||||||
|
fragment = compose["services"]["decky-01-http"]
|
||||||
|
assert fragment["build"]["args"]["BASE_IMAGE"] == expected_build_base
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# BASE_IMAGE NOT injected for upstream-image services
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("svc", UPSTREAM_SERVICES)
|
||||||
|
def test_upstream_service_has_no_build_section(svc):
|
||||||
|
"""Upstream-image services must not receive a build section or BASE_IMAGE."""
|
||||||
|
config = _make_config([svc])
|
||||||
|
compose = generate_compose(config)
|
||||||
|
fragment = compose["services"][f"decky-01-{svc}"]
|
||||||
|
assert "build" not in fragment
|
||||||
|
assert "image" in fragment
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Base container uses distro image, not build_base
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("distro", list(DISTROS.keys()))
|
||||||
|
def test_base_container_uses_full_distro_image(distro):
|
||||||
|
"""The IP-holder base container must use distro.image, not build_base."""
|
||||||
|
config = _make_config(["ssh"], distro=distro)
|
||||||
|
compose = generate_compose(config)
|
||||||
|
base = compose["services"]["decky-01"]
|
||||||
|
expected = DISTROS[distro].image
|
||||||
|
assert base["image"] == expected, (
|
||||||
|
f"distro={distro}: base container image '{base['image']}' != '{expected}'"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Distro profile — build_base is always apt-compatible
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_all_distros_have_build_base():
|
||||||
|
for slug, profile in all_distros().items():
|
||||||
|
assert profile.build_base, f"Distro '{slug}' has empty build_base"
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_distro_build_bases_are_apt_compatible():
|
||||||
|
for slug, profile in all_distros().items():
|
||||||
|
assert profile.build_base in APT_COMPATIBLE, (
|
||||||
|
f"Distro '{slug}' build_base '{profile.build_base}' is not apt-compatible. "
|
||||||
|
f"Allowed: {APT_COMPATIBLE}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Heterogeneity — multiple deckies with different distros get different images
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def test_multiple_deckies_different_build_bases():
|
||||||
|
"""A multi-decky deployment with ubuntu22 and debian must differ in BASE_IMAGE."""
|
||||||
|
deckies = [
|
||||||
|
DeckyConfig(
|
||||||
|
name="decky-01", ip="10.0.0.10",
|
||||||
|
services=["http"], distro="debian",
|
||||||
|
base_image="debian:bookworm-slim", build_base="debian:bookworm-slim",
|
||||||
|
hostname="host-01",
|
||||||
|
),
|
||||||
|
DeckyConfig(
|
||||||
|
name="decky-02", ip="10.0.0.11",
|
||||||
|
services=["http"], distro="ubuntu22",
|
||||||
|
base_image="ubuntu:22.04", build_base="ubuntu:22.04",
|
||||||
|
hostname="host-02",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
config = DecnetConfig(
|
||||||
|
mode="unihost", interface="eth0",
|
||||||
|
subnet="10.0.0.0/24", gateway="10.0.0.1",
|
||||||
|
deckies=deckies,
|
||||||
|
)
|
||||||
|
compose = generate_compose(config)
|
||||||
|
|
||||||
|
base_img_01 = compose["services"]["decky-01-http"]["build"]["args"]["BASE_IMAGE"]
|
||||||
|
base_img_02 = compose["services"]["decky-02-http"]["build"]["args"]["BASE_IMAGE"]
|
||||||
|
|
||||||
|
assert base_img_01 == "debian:bookworm-slim"
|
||||||
|
assert base_img_02 == "ubuntu:22.04"
|
||||||
|
assert base_img_01 != base_img_02
|
||||||
Reference in New Issue
Block a user