Protocol Reference¶
This document provides comprehensive reference documentation for all protocols (interfaces) in the Hop3 plugin system.
All protocols are defined in hop3/core/protocols.py using Python's PEP 544 Protocol typing.
Table of Contents¶
- Data Structures
- DeploymentContext
- BuildArtifact
- DeploymentInfo
- Strategy Protocols
- Builder
- Deployer
- Addon
- Proxy
- BaseProxy
- OS
Data Structures¶
DeploymentContext¶
Location: hop3.core.protocols.DeploymentContext
Purpose: Carries contextual information about an application deployment.
Attributes:
| Attribute | Type | Description |
|---|---|---|
app_name |
str |
Name of the application being deployed |
source_path |
Path |
Path to application source code |
app_config |
dict |
Application configuration from hop3.toml |
app |
App \| None |
Optional full App database object |
Usage:
from pathlib import Path
from hop3.core.protocols import DeploymentContext
context = DeploymentContext(
app_name="myapp",
source_path=Path("/home/hop3/apps/myapp/src"),
app_config={"workers": {"web": "gunicorn app:app"}},
app=app_instance # Optional
)
Validation: The source_path must be a directory (validated in __post_init__).
BuildArtifact¶
Location: hop3.core.protocols.BuildArtifact
Purpose: Represents the output of a build process.
Attributes:
| Attribute | Type | Description |
|---|---|---|
kind |
str |
Type of artifact: "virtualenv", "docker-image", "buildpack", etc. |
location |
str |
Path or reference to the artifact (e.g., "/path/to/venv", "image:tag") |
metadata |
dict[str, Any] |
Additional information about the artifact (versions, sizes, etc.) |
Usage:
from hop3.core.protocols import BuildArtifact
# Virtualenv artifact
artifact = BuildArtifact(
kind="virtualenv",
location="/home/hop3/apps/myapp/venv",
metadata={"python_version": "3.11.2"}
)
# Docker image artifact
artifact = BuildArtifact(
kind="docker-image",
location="myapp:v1.2.3",
metadata={"image_id": "sha256:abc123", "size_mb": 145}
)
DeploymentInfo¶
Location: hop3.core.protocols.DeploymentInfo
Purpose: Information returned by a deployment strategy about where the app is running.
Attributes:
| Attribute | Type | Description |
|---|---|---|
protocol |
str |
Protocol used: "http", "https", "tcp", "unix" |
address |
str |
IP address or hostname where app is accessible |
port |
int \| None |
Port number (None for Unix sockets) |
Usage:
from hop3.core.protocols import DeploymentInfo
# HTTP deployment
info = DeploymentInfo(
protocol="http",
address="127.0.0.1",
port=8080
)
# Unix socket deployment
info = DeploymentInfo(
protocol="unix",
address="/tmp/myapp.sock",
port=None
)
Strategy Protocols¶
Builder¶
Location: hop3.core.protocols.Builder
Purpose: Convert source code into a runnable artifact.
Required Attributes:
| Attribute | Type | Description |
|---|---|---|
name |
str |
Unique identifier (e.g., "python", "docker", "node") |
context |
DeploymentContext |
Deployment context with app information |
Required Methods:
accept() -> bool¶
Determine if this strategy can build the application.
Returns: True if the strategy can build this app, False otherwise.
Implementation Guidelines: - Check for framework-specific files (requirements.txt, package.json, etc.) - Check configuration settings - Verify required tools are available - Should be fast (no expensive operations)
Example:
def accept(self) -> bool:
"""Accept if requirements.txt or pyproject.toml exists."""
src_path = self.context.source_path
return (src_path / "requirements.txt").exists() or \
(src_path / "pyproject.toml").exists()
build() -> BuildArtifact¶
Execute the build process and return artifact information.
Returns: BuildArtifact describing what was built.
Raises:
- Abort (from hop3.lib) for user-facing errors
- Other exceptions for unexpected errors
Implementation Guidelines: - Create isolated build environment (virtualenv, container, etc.) - Install dependencies - Compile code if needed - Verify build succeeded - Return artifact details
Example:
def build(self) -> BuildArtifact:
"""Build Python virtualenv."""
venv_path = self._create_virtualenv()
self._install_dependencies(venv_path)
return BuildArtifact(
kind="virtualenv",
location=str(venv_path),
metadata={"python_version": self._get_python_version()}
)
Complete Example:
See packages/hop3-server/src/hop3/builders/python.py for the canonical Python builder implementation.
Deployer¶
Location: hop3.core.protocols.Deployer
Purpose: Run a build artifact and manage its lifecycle.
Required Attributes:
| Attribute | Type | Description |
|---|---|---|
name |
str |
Unique identifier (e.g., "uwsgi", "docker-compose", "systemd") |
context |
DeploymentContext |
Deployment context |
artifact |
BuildArtifact |
Build artifact to deploy |
Required Methods:
accept() -> bool¶
Determine if this strategy can deploy the given artifact.
Returns: True if the strategy can deploy this artifact type.
Example:
def accept(self) -> bool:
"""Accept docker-image artifacts."""
return self.artifact.kind == "docker-image"
deploy(deltas: dict[str, int] | None = None) -> DeploymentInfo¶
Deploy the artifact.
Parameters:
- deltas: Optional dictionary of worker scaling changes ({"web": 2, "worker": 1})
Returns: DeploymentInfo with connection details for the proxy.
Implementation Guidelines: - Start application processes/containers - Configure workers based on deltas - Wait for startup (or return immediately if async) - Return connection information
Example:
def deploy(self, deltas=None) -> DeploymentInfo:
"""Deploy with uWSGI."""
self._write_uwsgi_config()
self._symlink_to_emperor() # uWSGI emperor auto-starts
return DeploymentInfo(
protocol="http",
address="127.0.0.1",
port=self.context.app.port
)
stop() -> None¶
Stop the running application.
Implementation Guidelines: - Gracefully shut down processes/containers - Clean up runtime resources (sockets, PID files) - Should be idempotent (safe to call multiple times)
Example:
def stop(self) -> None:
"""Stop by removing emperor symlink."""
config_file = Path(f"/etc/uwsgi/enabled/{self.context.app_name}.ini")
if config_file.exists():
config_file.unlink()
check_status() -> bool¶
Check if the application is actually running.
Returns: True if processes/containers are confirmed running, False otherwise.
Implementation Guidelines:
- Verify actual running state (don't just check config files)
- For uWSGI: check socket files, process listings, config files
- For Docker: check container status (docker ps)
- For systemd: check service status (systemctl is-active)
- Should be reliable and fast
- Should not raise exceptions (return False on errors)
Example:
def check_status(self) -> bool:
"""Check if Docker containers are running."""
result = subprocess.run(
["docker", "compose", "ps", "--format", "{{.State}}"],
cwd=self.context.source_path,
capture_output=True,
text=True,
timeout=5
)
if result.returncode != 0:
return False
return "running" in result.stdout.lower()
scale(deltas: dict[str, int] | None = None) -> None¶
Scale workers up or down.
Parameters:
- deltas: Dictionary mapping worker names to scaling changes (e.g., {"web": 2} to set 2 web workers)
Implementation Guidelines: - Adjust number of worker processes/containers - May trigger restart/reload - Should be graceful (no downtime if possible)
Example:
def scale(self, deltas=None):
"""Scale Docker Compose services."""
if not deltas:
return
scale_args = []
for service, count in deltas.items():
scale_args.extend(["--scale", f"{service}={count}"])
cmd = ["docker", "compose", "up", "-d"] + scale_args
subprocess.run(cmd, cwd=self.context.source_path, check=True)
Optional Methods:
These methods may be provided for additional functionality:
start() -> None: Start a stopped applicationrestart() -> None: Restart a running applicationget_status() -> dict: Get detailed status information
Complete Example:
See packages/hop3-server/src/hop3/plugins/docker/deployer.py for the Docker Compose implementation.
Addon¶
Location: hop3.core.protocols.Addon
Purpose: Manage backing services (databases, caches, message queues, etc.).
Required Attributes:
| Attribute | Type | Description |
|---|---|---|
name |
str |
Service type (e.g., "postgres", "redis", "mysql") |
service_name |
str |
Specific instance name for this service |
Required Methods:
create() -> None¶
Create a new service instance.
Implementation Guidelines: - Provision necessary resources (database, user, directories) - Configure service - Start service - Should be idempotent
Example:
def create(self) -> None:
"""Create PostgreSQL database."""
# Check if already exists
if self._database_exists():
log(f"Database {self.addon_name} already exists", fg="yellow")
return
# Create database and user
subprocess.run([
"sudo", "-u", "postgres", "createdb", self.addon_name
], check=True)
destroy() -> None¶
Destroy the service instance and all its data.
Implementation Guidelines: - Stop service - Remove all data (WARNING: destructive) - Remove configuration - Should be idempotent
Example:
def destroy(self) -> None:
"""Destroy PostgreSQL database."""
subprocess.run([
"sudo", "-u", "postgres", "dropdb", "--if-exists", self.addon_name
])
get_connection_details() -> dict[str, str]¶
Get environment variables for applications to connect to this service.
Returns: Dictionary mapping environment variable names to values.
Implementation Guidelines: - Return standard connection URLs (DATABASE_URL, REDIS_URL, etc.) - Include all necessary connection parameters - Use localhost for local services
Example:
def get_connection_details(self) -> dict[str, str]:
"""Return PostgreSQL connection details."""
return {
"DATABASE_URL": f"postgresql://hop3:password@localhost/{self.addon_name}",
"POSTGRES_DB": self.addon_name,
"POSTGRES_HOST": "localhost",
"POSTGRES_PORT": "5432"
}
backup() -> Path¶
Create a backup of service data.
Returns: Path to the backup file or directory.
Implementation Guidelines: - Create complete backup of data - Use service-specific backup tools (pg_dump, redis-cli SAVE, etc.) - Include timestamps in filename - Store in predictable location
Example:
def backup(self) -> Path:
"""Backup PostgreSQL database."""
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_file = Path(f"/home/hop3/backups/postgres_{self.addon_name}_{timestamp}.sql")
with backup_file.open("w") as f:
subprocess.run([
"sudo", "-u", "postgres", "pg_dump", self.addon_name
], stdout=f, check=True)
return backup_file
restore(backup_path: Path) -> None¶
Restore service data from a backup.
Parameters:
- backup_path: Path to backup file/directory to restore from
Implementation Guidelines: - Stop service if necessary - Clear existing data (optional, based on strategy) - Restore from backup - Restart service
Example:
def restore(self, backup_path: Path) -> None:
"""Restore PostgreSQL database."""
# Drop and recreate database
self.destroy()
self.create()
# Restore from backup
with backup_path.open() as f:
subprocess.run([
"sudo", "-u", "postgres", "psql", self.addon_name
], stdin=f, check=True)
info() -> dict[str, Any]¶
Get information about the service instance.
Returns: Dictionary with service details.
Implementation Guidelines: - Include status, version, size, etc. - Don't include sensitive data (passwords) - Should be fast
Example:
def info(self) -> dict[str, Any]:
"""Get PostgreSQL database info."""
# Get database size
result = subprocess.run([
"sudo", "-u", "postgres", "psql", "-c",
f"SELECT pg_database_size('{self.addon_name}');"
], capture_output=True, text=True)
return {
"service_type": "postgres",
"service_name": self.addon_name,
"status": "running" if self._is_running() else "stopped",
"version": self._get_version(),
"size_bytes": self._parse_size(result.stdout)
}
Complete Example:
See packages/hop3-server/src/hop3/plugins/postgresql/service.py for the PostgreSQL service implementation.
Proxy¶
Location: hop3.core.protocols.Proxy
Purpose: Basic protocol interface for reverse proxies (legacy).
Note: Most proxy implementations should use BaseProxy instead, which provides common functionality.
Required Attributes:
| Attribute | Type | Description |
|---|---|---|
app |
App |
Application database object |
env |
Env |
Environment variables |
workers |
dict[str, str] |
Worker configuration from Procfile |
Required Methods:
setup() -> None¶
Configure the proxy for this application.
BaseProxy¶
Location: hop3.core.protocols.BaseProxy
Purpose: Abstract base class for proxy implementations (Nginx, Caddy, Traefik, etc.).
Inheritance: Concrete proxy classes should inherit from BaseProxy and implement the abstract methods.
Attributes (provided by base class):
| Attribute | Type | Description |
|---|---|---|
app |
App |
Application database object |
env |
Env |
Environment variables |
workers |
dict[str, str] |
Worker configuration |
Properties (provided by base class):
| Property | Type | Description |
|---|---|---|
app_name |
str |
Application name (from app.name) |
app_path |
Path |
Application directory (from app.app_path) |
src_path |
Path |
Source directory (from app.src_path) |
Abstract Methods (must be implemented):
get_proxy_name() -> str¶
Return the proxy name for environment variable prefixes.
Returns: One of "nginx", "caddy", "traefik", etc.
Example:
setup_backend() -> None¶
Configure the backend connection (TCP port or Unix socket).
Implementation Guidelines:
- Set NGINX_SOCKET or NGINX_BACKEND environment variables
- Choose between Unix socket and TCP based on configuration
Example:
def setup_backend(self) -> None:
"""Configure Nginx backend."""
if self.env.get("NGINX_SOCKET"):
socket_path = f"/tmp/{self.app_name}.sock"
self.update_env("NGINX_SOCKET", socket_path)
else:
port = self.app.port
self.update_env("NGINX_BACKEND", f"127.0.0.1:{port}")
setup_certificates() -> None¶
Setup SSL/TLS certificates for the application.
Implementation Guidelines: - Generate self-signed certificates for development - Integrate with Let's Encrypt for production - Set certificate paths in environment
setup_cache() -> None¶
Configure caching for the application.
Implementation Guidelines: - Enable/disable based on environment variables - Configure cache paths - Set cache parameters (size, TTL, etc.)
setup_static() -> None¶
Configure static file serving.
Implementation Guidelines:
- Use self.get_static_paths() helper method
- Generate location/route blocks for each static path
- Configure caching headers
extra_setup() -> None¶
Perform additional proxy-specific setup.
Implementation Guidelines: - Custom headers - Redirect rules - Rate limiting - Access controls
generate_config() -> None¶
Generate the proxy configuration file.
Implementation Guidelines:
- Use templates (Jinja2 recommended)
- Render with self.env and self.app context
- Write to proxy-specific config directory
check_config() -> None¶
Validate the generated configuration.
Implementation Guidelines:
- Use proxy's config test command (nginx -t, etc.)
- Raise exception if validation fails
reload_proxy() -> None¶
Reload the proxy to apply configuration changes.
Implementation Guidelines:
- Try supervisor control first
- Fall back to systemctl
- Fall back to direct command (nginx -s reload)
- Should be graceful (no downtime)
Provided Methods (from base class):
update_env(key: str, value: str = "", template: str = "") -> None¶
Update an environment variable, optionally from a template.
Parameters:
- key: Environment variable name
- value: Value to set (if template is empty)
- template: Template string to format with current env vars
Example:
# Set directly
self.update_env("NGINX_BACKEND", "127.0.0.1:8080")
# Use template
self.update_env("CACHE_PATH", template="/var/cache/{app_name}")
setup() -> None¶
Main setup orchestrator (already implemented, don't override).
This method calls all setup methods in the correct order:
1. setup_backend()
2. setup_certificates()
3. setup_cache()
4. setup_static()
5. extra_setup()
6. generate_config()
7. check_config()
8. reload_proxy()
get_static_paths() -> list[tuple[str, Path]]¶
Get static URL-to-filesystem mappings.
Returns: List of tuples (url_prefix, filesystem_path).
Implementation: Reads from {PROXY_NAME}_STATIC_PATHS environment variable.
Format: /url:filesystem/path,/url2:path2
Example:
# If NGINX_STATIC_PATHS="/static:static/,/media:media/"
static_paths = self.get_static_paths()
# Returns: [("/static", Path(".../static")), ("/media", Path(".../media"))]
Complete Example:
See packages/hop3-server/src/hop3/plugins/proxy/nginx/_setup.py for the Nginx implementation.
OS¶
Location: hop3.core.protocols.OS
Purpose: Handle operating system-specific server setup and package management.
Required Attributes:
| Attribute | Type | Description |
|---|---|---|
name |
str |
OS identifier (e.g., "debian12", "ubuntu2204") |
display_name |
str |
Human-readable name (e.g., "Debian 12 (Bookworm)") |
packages |
list[str] |
Required system packages for hop3 |
Required Methods:
detect() -> bool¶
Check if this strategy matches the current operating system.
Returns: True if this OS strategy should be used on the current system.
Implementation Guidelines:
- Read /etc/os-release or similar files
- Check distribution name and version
- Return True only for exact matches or families
Example:
def detect(self) -> bool:
"""Check if this is Debian 12."""
if not Path("/etc/os-release").exists():
return False
os_release = Path("/etc/os-release").read_text()
return "debian" in os_release.lower() and "12" in os_release
setup_server() -> None¶
Install dependencies and configure the system for hop3.
Implementation Guidelines: 1. Configure package manager settings 2. Create hop3 user account 3. Install required system packages 4. Set up directories and permissions 5. Should be idempotent
Example:
def setup_server(self) -> None:
"""Setup Debian server."""
# Update package lists
subprocess.run(["apt-get", "update"], check=True)
# Create hop3 user
self.ensure_user("hop3", "/home/hop3", "/bin/bash", "hop3")
# Install packages
self.ensure_packages(self.packages)
# Additional setup
self._configure_firewall()
ensure_packages(packages: list[str], *, update: bool = True) -> None¶
Install system packages using the OS package manager.
Parameters:
- packages: List of package names to install
- update: Whether to update package lists first (default: True)
Implementation Guidelines: - Use OS-specific package manager (apt, yum, dnf, pacman, etc.) - Should be idempotent (don't reinstall if already installed) - Handle errors gracefully
Example:
def ensure_packages(self, packages, *, update=True):
"""Install packages with apt."""
if update:
subprocess.run(["apt-get", "update"])
subprocess.run(
["apt-get", "install", "-y", "--no-install-recommends"] + packages,
check=True
)
ensure_user(user: str, home: str, shell: str, group: str) -> None¶
Create a system user account if it doesn't exist.
Parameters:
- user: Username to create
- home: Home directory path
- shell: Default shell path
- group: Primary group name
Implementation Guidelines: - Check if user already exists - Create user with specified settings - Should be idempotent
Example:
def ensure_user(self, user, home, shell, group):
"""Create user if not exists."""
try:
subprocess.run(["id", user], check=True, capture_output=True)
# User exists, nothing to do
except subprocess.CalledProcessError:
# User doesn't exist, create it
subprocess.run([
"useradd",
"-m", # Create home directory
"-d", home, # Home directory path
"-s", shell, # Login shell
"-U", # Create group with same name
user
], check=True)
Complete Example:
See packages/hop3-server/src/hop3/plugins/oses/debian_family.py for the Debian/Ubuntu implementation.
Protocol Compliance Checklist¶
When implementing a strategy, use this checklist to ensure protocol compliance:
Builder¶
-
nameattribute set to unique string -
contextattribute assigned in__init__ -
accept()method returns bool -
accept()is fast (no expensive operations) -
build()returnsBuildArtifact -
build()raisesAbortfor user-facing errors - Build creates isolated environment
- Build is reproducible
Deployer¶
-
nameattribute set to unique string -
contextandartifactattributes assigned -
accept()checksartifact.kind -
deploy()returnsDeploymentInfo -
deploy()handles optionaldeltasparameter -
stop()is idempotent -
check_status()verifies actual running state -
check_status()doesn't raise exceptions -
scale()handles worker scaling
Addon¶
-
nameattribute set (service type) -
service_nameattribute set (instance name) -
create()is idempotent -
destroy()removes all data -
get_connection_details()returns connection URLs -
backup()returns Path to backup file -
restore()accepts Path parameter -
info()returns status dictionary
BaseProxy¶
- Inherits from
BaseProxy - Uses
@dataclass(frozen=True)decorator -
get_proxy_name()returns lowercase name - All abstract methods implemented
-
setup()not overridden (use template method) -
reload_proxy()is graceful - Uses
get_static_paths()helper
OS¶
-
nameattribute set (OS identifier) -
display_nameattribute set -
packageslist defined -
detect()checks/etc/os-release -
setup_server()is idempotent -
ensure_packages()handles update parameter -
ensure_user()is idempotent
See Also¶
- Plugin Development Guide - How to create plugins
- Hook Specifications - How to register strategies
- External Plugin Guide - Publishing external plugins