mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-11-02 19:02:31 +00:00
Merge pull request #538 from jcastro/feature/proxmenux-oci-next-10
Add ten OCI application profiles
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
"""Guard the Campfire recipe contract in the next-10 catalog profiles."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
EXPECTED_IMAGE = "ghcr.io/awsomefox/campfire:latest"
|
||||
EXPECTED_ICON = (
|
||||
"https://raw.githubusercontent.com/AwsomeFox/campfire/main/"
|
||||
"apps/web/public/pwa-512x512.png"
|
||||
)
|
||||
|
||||
|
||||
class CampfireProfileTests(unittest.TestCase):
|
||||
def test_app_and_curated_profiles_match_the_single_service_contract(self):
|
||||
for family in ("apps", "curated"):
|
||||
with self.subTest(family=family):
|
||||
template = json.loads(
|
||||
(ROOT / "catalog" / family / "campfire.json")
|
||||
.read_text(encoding="utf-8")
|
||||
)
|
||||
|
||||
contract = template["container_contract"]
|
||||
stack = template["compose_stack"]
|
||||
self.assertEqual(contract["image"]["reference"], EXPECTED_IMAGE)
|
||||
self.assertEqual(stack["service_count"], 1)
|
||||
self.assertEqual(len(stack["services"]), 1)
|
||||
service = stack["services"][0]
|
||||
self.assertEqual(service["image"], EXPECTED_IMAGE)
|
||||
self.assertEqual(service["compose"]["image"], EXPECTED_IMAGE)
|
||||
|
||||
volume_paths = {
|
||||
volume["container_path"] for volume in contract["volumes"]
|
||||
}
|
||||
self.assertIn("/data", volume_paths)
|
||||
self.assertIn("./data-0:/data", service["compose"]["volumes"])
|
||||
|
||||
setup = next(
|
||||
endpoint for endpoint in template["first_run"]["endpoints"]
|
||||
if endpoint["label"] == "Campfire first-run setup"
|
||||
)
|
||||
self.assertEqual(setup["path"], "/auth/setup")
|
||||
|
||||
healthcheck = template["proxmox"]["installer_profile"][
|
||||
"startup_healthcheck"
|
||||
]
|
||||
self.assertEqual(healthcheck["path"], "/readyz")
|
||||
|
||||
self.assertEqual(template["catalog_ui"]["icon"], EXPECTED_ICON)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,156 @@
|
||||
"""Static install contracts for the next OCI catalog profiles."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
PROFILES = {
|
||||
"chhoto-url": {
|
||||
"image": "sintan1729/chhoto-url:latest",
|
||||
"data_path": "/data",
|
||||
"port": 4567,
|
||||
"health_path": "/",
|
||||
},
|
||||
"openhabittracker": {
|
||||
"image": "jinjinov/openhabittracker:latest",
|
||||
"data_path": "/app/.OpenHabitTracker",
|
||||
"port": 8080,
|
||||
"health_path": "/login",
|
||||
},
|
||||
"watcharr": {
|
||||
"image": "ghcr.io/sbondco/watcharr:latest",
|
||||
"data_path": "/data",
|
||||
"port": 3080,
|
||||
"health_path": "/",
|
||||
},
|
||||
"ech0": {
|
||||
"image": "sn0wl1n/ech0:latest",
|
||||
"data_path": "/app/data",
|
||||
"port": 6277,
|
||||
"health_path": "/",
|
||||
},
|
||||
"restic-rest-server": {
|
||||
"image": "restic/rest-server:latest",
|
||||
"data_path": "/data",
|
||||
"port": 8000,
|
||||
"health_path": "/",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def load_profile(app_id):
|
||||
path = ROOT / "catalog" / "apps" / f"{app_id}.json"
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
class Next10BProfiles(unittest.TestCase):
|
||||
def test_each_profile_is_one_image_with_an_icon_and_persistent_data(self):
|
||||
for app_id, expected in PROFILES.items():
|
||||
with self.subTest(app=app_id):
|
||||
profile = load_profile(app_id)
|
||||
services = profile["compose_stack"]["services"]
|
||||
image = profile["container_contract"]["image"]["reference"]
|
||||
|
||||
self.assertEqual(profile["compose_stack"]["service_count"], 1)
|
||||
self.assertEqual(len(services), 1)
|
||||
self.assertEqual(services[0]["image"], expected["image"])
|
||||
self.assertEqual(image, expected["image"])
|
||||
self.assertEqual(services[0]["depends_on"], [])
|
||||
|
||||
icon = profile["catalog_ui"].get("icon")
|
||||
self.assertIsInstance(icon, str)
|
||||
self.assertTrue(icon.startswith("https://"), icon)
|
||||
|
||||
mount_paths = {
|
||||
volume["container_path"]
|
||||
for volume in profile["container_contract"]["volumes"]
|
||||
}
|
||||
self.assertIn(expected["data_path"], mount_paths)
|
||||
|
||||
def test_each_profile_has_a_matching_first_start_check(self):
|
||||
for app_id, expected in PROFILES.items():
|
||||
with self.subTest(app=app_id):
|
||||
profile = load_profile(app_id)
|
||||
check = profile["proxmox"]["installer_profile"]["startup_healthcheck"]
|
||||
|
||||
self.assertEqual(check["scheme"], "http")
|
||||
self.assertEqual(check["port"], expected["port"])
|
||||
self.assertEqual(check["path"], expected["health_path"])
|
||||
self.assertTrue(check["accept_any_status"])
|
||||
|
||||
def test_chhoto_url_uses_generated_password_and_data_database(self):
|
||||
profile = load_profile("chhoto-url")
|
||||
environment = {
|
||||
item["name"]: item
|
||||
for item in profile["container_contract"]["environment"]
|
||||
}
|
||||
generated = profile["proxmox"]["installer_profile"]["generated_sensitive_environment"]
|
||||
credential = profile["first_run"]["credentials"][0]
|
||||
|
||||
self.assertIn("CHHOTO_PASSWORD", environment)
|
||||
self.assertTrue(environment["CHHOTO_PASSWORD"]["sensitive"])
|
||||
self.assertIn("CHHOTO_PASSWORD", generated)
|
||||
self.assertEqual(credential["password_environment"], "CHHOTO_PASSWORD")
|
||||
self.assertEqual(environment["CHHOTO_DB_URL"]["example"], "/data/urls.sqlite")
|
||||
self.assertEqual(environment["CHHOTO_SQLITE_USE_WAL_MODE"]["example"], "True")
|
||||
|
||||
def test_openhabittracker_prompts_for_user_and_generates_password_and_jwt(self):
|
||||
profile = load_profile("openhabittracker")
|
||||
environment = {
|
||||
item["name"]: item
|
||||
for item in profile["container_contract"]["environment"]
|
||||
}
|
||||
generated = profile["proxmox"]["installer_profile"]["generated_sensitive_environment"]
|
||||
credential = profile["first_run"]["credentials"][0]
|
||||
|
||||
self.assertIn("prompt", environment["AppSettings__UserName"])
|
||||
self.assertIn("prompt", environment["AppSettings__Email"])
|
||||
self.assertIn("AppSettings__Password", generated)
|
||||
self.assertIn("AppSettings__JwtSecret", generated)
|
||||
self.assertEqual(credential["username_environment"], "AppSettings__UserName")
|
||||
self.assertEqual(credential["password_environment"], "AppSettings__Password")
|
||||
|
||||
def test_watcharr_first_registered_user_becomes_admin(self):
|
||||
profile = load_profile("watcharr")
|
||||
credentials = profile["first_run"]["credentials"]
|
||||
|
||||
self.assertEqual(profile["container_contract"]["environment"], [])
|
||||
self.assertEqual(len(credentials), 1)
|
||||
self.assertIn("administrator", credentials[0]["label"].lower())
|
||||
self.assertIn("first-run setup", credentials[0]["username"].lower())
|
||||
|
||||
def test_ech0_generates_jwt_and_marks_first_user_as_owner(self):
|
||||
profile = load_profile("ech0")
|
||||
generated = profile["proxmox"]["installer_profile"]["generated_sensitive_environment"]
|
||||
credentials = profile["first_run"]["credentials"]
|
||||
|
||||
self.assertIn("JWT_SECRET", generated)
|
||||
self.assertEqual(profile["container_contract"]["environment"][0]["name"], "JWT_SECRET")
|
||||
self.assertEqual(len(credentials), 1)
|
||||
self.assertIn("Owner", credentials[0]["label"])
|
||||
self.assertIn("first-run setup", credentials[0]["username"].lower())
|
||||
|
||||
def test_restic_server_is_an_api_without_a_browser_endpoint(self):
|
||||
profile = load_profile("restic-rest-server")
|
||||
credentials = profile["first_run"]["credentials"]
|
||||
ui = profile["catalog_ui"]
|
||||
|
||||
self.assertEqual(profile["first_run"]["endpoints"], [])
|
||||
self.assertIsNone(ui["launch"]["port"])
|
||||
self.assertIn("no web interface", ui["tagline"]["en_US"].lower())
|
||||
self.assertIn("not a browser-based application", ui["description"]["en_US"].lower())
|
||||
self.assertEqual(len(credentials), 1)
|
||||
self.assertIn("create_user", credentials[0]["username"])
|
||||
self.assertIsNone(credentials[0]["password"])
|
||||
self.assertEqual(
|
||||
profile["container_contract"]["environment"][0]["example"],
|
||||
"--private-repos",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,176 @@
|
||||
"""Guard the Activepieces, Bytebase, Metabase and Kopia OCI recipes."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
EXPECTED = {
|
||||
"activepieces": {
|
||||
"image": "ghcr.io/activepieces/activepieces:latest",
|
||||
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/activepieces.webp",
|
||||
"health": (80, "/api/v1/health"),
|
||||
"note_terms": ("personal", "memory"),
|
||||
},
|
||||
"bytebase": {
|
||||
"image": "bytebase/bytebase:latest",
|
||||
"icon": "https://cdn.jsdelivr.net/gh/bytebase/bytebase@main/frontend/src/assets/logo-icon.svg",
|
||||
"health": (8080, "/healthz"),
|
||||
"note_terms": (),
|
||||
},
|
||||
"metabase": {
|
||||
"image": "metabase/metabase:v0.64-lts",
|
||||
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/metabase.webp",
|
||||
"health": (3000, "/api/health"),
|
||||
"note_terms": ("h2", "postgres"),
|
||||
},
|
||||
"kopia": {
|
||||
"image": "kopia/kopia:latest",
|
||||
"icon": "https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/kopia.webp",
|
||||
"health": (51515, "/"),
|
||||
"note_terms": ("repository", "password"),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def read_profile(family, app):
|
||||
path = ROOT / "catalog" / family / f"{app}.json"
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
class Next10CProfileTests(unittest.TestCase):
|
||||
def test_image_single_service_and_icon_match_catalog_contract(self):
|
||||
for app, expected in EXPECTED.items():
|
||||
with self.subTest(app=app):
|
||||
for family in ("apps", "curated"):
|
||||
profile = read_profile(family, app)
|
||||
stack = profile["compose_stack"]
|
||||
service = stack["services"][0]
|
||||
self.assertEqual(
|
||||
profile["container_contract"]["image"]["reference"],
|
||||
expected["image"],
|
||||
)
|
||||
self.assertEqual(stack["service_count"], 1)
|
||||
self.assertEqual(len(stack["services"]), 1)
|
||||
self.assertEqual(service["image"], expected["image"])
|
||||
self.assertEqual(service["compose"]["image"], expected["image"])
|
||||
self.assertEqual(profile["catalog_ui"]["icon"], expected["icon"])
|
||||
self.assertTrue(expected["icon"].startswith("https://"))
|
||||
|
||||
def test_startup_healthcheck_targets_the_documented_service_endpoint(self):
|
||||
for app, expected in EXPECTED.items():
|
||||
with self.subTest(app=app):
|
||||
for family in ("apps", "curated"):
|
||||
profile = read_profile(family, app)
|
||||
health = profile["proxmox"]["installer_profile"][
|
||||
"startup_healthcheck"
|
||||
]
|
||||
self.assertEqual(health["port"], expected["health"][0])
|
||||
self.assertEqual(health["path"], expected["health"][1])
|
||||
self.assertEqual(health["scheme"], "http")
|
||||
|
||||
def test_required_persistent_paths_are_mounted_by_the_single_service(self):
|
||||
for app in EXPECTED:
|
||||
with self.subTest(app=app):
|
||||
for family in ("apps", "curated"):
|
||||
profile = read_profile(family, app)
|
||||
contract_paths = {
|
||||
volume["container_path"]
|
||||
for volume in profile["container_contract"]["volumes"]
|
||||
if volume.get("required", False)
|
||||
}
|
||||
mounts = profile["compose_stack"]["services"][0]["compose"].get(
|
||||
"volumes", []
|
||||
)
|
||||
mounted_paths = {
|
||||
mount.split(":")[1]
|
||||
for mount in mounts
|
||||
if len(mount.split(":")) >= 2
|
||||
}
|
||||
self.assertTrue(contract_paths, "profile must declare persistent data")
|
||||
self.assertTrue(
|
||||
contract_paths.issubset(mounted_paths),
|
||||
f"required paths missing from Compose mounts: "
|
||||
f"{sorted(contract_paths - mounted_paths)}",
|
||||
)
|
||||
|
||||
def test_secret_environment_is_sensitive_generated_and_wired_to_compose(self):
|
||||
for app in EXPECTED:
|
||||
with self.subTest(app=app):
|
||||
for family in ("apps", "curated"):
|
||||
profile = read_profile(family, app)
|
||||
declared_sensitive = {
|
||||
item["name"]
|
||||
for item in profile["container_contract"].get("environment", [])
|
||||
if item.get("sensitive")
|
||||
}
|
||||
runtime_generated = profile["proxmox"]["installer_profile"].get(
|
||||
"generated_sensitive_environment", {}
|
||||
)
|
||||
generated_bindings = {
|
||||
binding["environment_variable"]
|
||||
for secret in profile["compose_stack"]["installer_inputs"].get(
|
||||
"generated_secrets", []
|
||||
)
|
||||
for binding in secret.get("bindings", [])
|
||||
}
|
||||
compose_environment = profile["compose_stack"]["services"][0][
|
||||
"compose"
|
||||
].get("environment", {})
|
||||
|
||||
if app == "bytebase":
|
||||
self.assertEqual(generated_bindings, set())
|
||||
self.assertEqual(runtime_generated, {})
|
||||
self.assertNotIn("HBOX_AUTH_API_KEY_PEPPER", declared_sensitive)
|
||||
self.assertNotIn("HBOX_AUTH_API_KEY_PEPPER", compose_environment)
|
||||
self.assertNotIn("homebox", json.dumps(profile).casefold())
|
||||
continue
|
||||
|
||||
self.assertTrue(
|
||||
generated_bindings,
|
||||
"profile must document generated credentials/secrets",
|
||||
)
|
||||
self.assertEqual(
|
||||
generated_bindings,
|
||||
set(runtime_generated),
|
||||
"generated secret bindings and runtime generators must match",
|
||||
)
|
||||
self.assertTrue(
|
||||
generated_bindings.issubset(declared_sensitive),
|
||||
"every generated secret must be declared sensitive in the contract",
|
||||
)
|
||||
for variable in generated_bindings:
|
||||
self.assertIn(variable, compose_environment)
|
||||
self.assertIn(variable, str(compose_environment[variable]))
|
||||
|
||||
def test_app_notes_explain_the_key_limitations_or_setup_requirements(self):
|
||||
for app, expected in EXPECTED.items():
|
||||
with self.subTest(app=app):
|
||||
for family in ("apps", "curated"):
|
||||
profile = read_profile(family, app)
|
||||
if app == "bytebase":
|
||||
endpoints = profile["first_run"]["endpoints"]
|
||||
web_ui = next(
|
||||
endpoint for endpoint in endpoints
|
||||
if endpoint["label"] == "Web UI"
|
||||
)
|
||||
self.assertEqual(web_ui["scheme"], "http")
|
||||
self.assertEqual(web_ui["port"], 8080)
|
||||
self.assertEqual(web_ui["path"], "/")
|
||||
self.assertEqual(profile["first_run"]["credentials"], [])
|
||||
self.assertEqual(
|
||||
profile["catalog_ui"]["documentation"],
|
||||
"https://docs.bytebase.com/get-started/self-host/deploy-with-docker",
|
||||
)
|
||||
continue
|
||||
|
||||
tips = profile["catalog_ui"].get("tips", [])
|
||||
note = " ".join(tips).casefold()
|
||||
self.assertTrue(tips, "profile must include app-specific notes")
|
||||
for term in expected["note_terms"]:
|
||||
self.assertIn(term, note)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user