522 lines
12 KiB
Markdown
522 lines
12 KiB
Markdown
|
|
# Service Catalog Module: Reusable Abstraction
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
The Service Catalog system (implemented in syntaxis) can be **extracted and reused by any application** that needs to manage services, presets, and provisioning integration.
|
||
|
|
|
||
|
|
This document describes how to:
|
||
|
|
1. **Understand the abstraction** - What makes it reusable
|
||
|
|
2. **Implement in your app** - How to adapt it to other projects
|
||
|
|
3. **Integrate with provisioners** - How to connect with project-provisioning or similar systems
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## What Is a Service Catalog?
|
||
|
|
|
||
|
|
A **Service Catalog** is a **standardized TOML configuration** that completely describes:
|
||
|
|
|
||
|
|
1. **Available Services**: What can be installed/deployed
|
||
|
|
2. **Service Metadata**: How to run, health check, port, dependencies
|
||
|
|
3. **Presets**: Combinations of services for different contexts
|
||
|
|
4. **Patterns**: Common deployment topologies
|
||
|
|
5. **Networking**: Port assignments and protocols
|
||
|
|
|
||
|
|
**Key Principle**: One source of truth for service definitions that both local and remote systems can read independently.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## The Abstraction
|
||
|
|
|
||
|
|
### Core Components
|
||
|
|
|
||
|
|
```
|
||
|
|
Service Catalog System
|
||
|
|
├── Configuration Schema (TOML)
|
||
|
|
│ ├── Service definitions
|
||
|
|
│ ├── Preset compositions
|
||
|
|
│ ├── Service groups/patterns
|
||
|
|
│ └── Port mappings
|
||
|
|
├── Discovery Tool
|
||
|
|
│ ├── List services
|
||
|
|
│ ├── Show usage examples
|
||
|
|
│ ├── View preset compositions
|
||
|
|
│ └── Export for provisioners
|
||
|
|
└── Integration Bridge
|
||
|
|
├── Export presets as YAML
|
||
|
|
├── Generate provisioning configs
|
||
|
|
└── Validate deployments
|
||
|
|
```
|
||
|
|
|
||
|
|
### TOML Schema (Simplified)
|
||
|
|
|
||
|
|
```toml
|
||
|
|
[version]
|
||
|
|
catalog_version = "1.0.0"
|
||
|
|
|
||
|
|
[service.my-service]
|
||
|
|
name = "my-service"
|
||
|
|
display_name = "My Service"
|
||
|
|
description = "Service description"
|
||
|
|
type = "server" # or "cli", "database", "web", etc.
|
||
|
|
|
||
|
|
[service.my-service.server]
|
||
|
|
default_host = "127.0.0.1"
|
||
|
|
default_port = 3000
|
||
|
|
scheme = "http"
|
||
|
|
|
||
|
|
[service.my-service.usage]
|
||
|
|
basic_command = "my-service --bind 127.0.0.1:3000"
|
||
|
|
health_check_endpoint = "/health"
|
||
|
|
examples = [...]
|
||
|
|
|
||
|
|
[service.my-service.dependencies]
|
||
|
|
requires = []
|
||
|
|
conflicts = []
|
||
|
|
|
||
|
|
[preset.my-preset]
|
||
|
|
name = "My Preset"
|
||
|
|
description = "Preset description"
|
||
|
|
database_backend = "sqlite"
|
||
|
|
|
||
|
|
[preset.my-preset.services]
|
||
|
|
my_service = { enabled = true, auto_start = true, port = 3000 }
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Implementation Steps
|
||
|
|
|
||
|
|
### Step 1: Create the Catalog Schema
|
||
|
|
|
||
|
|
Create `configs/services-catalog.toml` in your project with this structure:
|
||
|
|
|
||
|
|
```toml
|
||
|
|
[version]
|
||
|
|
catalog_version = "1.0.0"
|
||
|
|
app_version = "1.0.0"
|
||
|
|
|
||
|
|
# Define all services
|
||
|
|
[service.app-api]
|
||
|
|
name = "app-api"
|
||
|
|
display_name = "API Server"
|
||
|
|
description = "REST API for the application"
|
||
|
|
type = "server"
|
||
|
|
required = false
|
||
|
|
|
||
|
|
[service.app-api.server]
|
||
|
|
default_host = "127.0.0.1"
|
||
|
|
default_port = 3000
|
||
|
|
scheme = "http"
|
||
|
|
|
||
|
|
[service.app-api.usage]
|
||
|
|
basic_command = "app-api --bind 127.0.0.1:3000"
|
||
|
|
health_check_endpoint = "/health"
|
||
|
|
examples = ["app-api --bind 127.0.0.1:3000"]
|
||
|
|
|
||
|
|
[service.app-api.dependencies]
|
||
|
|
requires = []
|
||
|
|
conflicts = []
|
||
|
|
|
||
|
|
[service.app-api.metadata]
|
||
|
|
platform_support = ["linux", "macos"]
|
||
|
|
min_memory_mb = 256
|
||
|
|
min_disk_space_mb = 100
|
||
|
|
|
||
|
|
# Define presets
|
||
|
|
[preset.local]
|
||
|
|
name = "Local Development"
|
||
|
|
description = "Single-machine development"
|
||
|
|
|
||
|
|
[preset.local.services]
|
||
|
|
app_api = { enabled = true, auto_start = false, port = 3000 }
|
||
|
|
|
||
|
|
[preset.production]
|
||
|
|
name = "Production"
|
||
|
|
description = "Production deployment"
|
||
|
|
|
||
|
|
[preset.production.services]
|
||
|
|
app_api = { enabled = true, auto_start = true, port = 3000 }
|
||
|
|
```
|
||
|
|
|
||
|
|
### Step 2: Create Discovery Tool
|
||
|
|
|
||
|
|
Create a simple script to help users explore the catalog:
|
||
|
|
|
||
|
|
**Option A: Simple Bash**
|
||
|
|
```bash
|
||
|
|
#!/bin/bash
|
||
|
|
# scripts/service-catalog.sh
|
||
|
|
|
||
|
|
echo "Available Services:"
|
||
|
|
grep '^\[service\.' configs/services-catalog.toml | sed 's/\[service\.//' | sed 's/\]//'
|
||
|
|
|
||
|
|
echo ""
|
||
|
|
echo "Available Presets:"
|
||
|
|
grep '^\[preset\.' configs/services-catalog.toml | sed 's/\[preset\.//' | sed 's/\]//'
|
||
|
|
```
|
||
|
|
|
||
|
|
**Option B: NuShell** (See syntax-catalog.nu for examples)
|
||
|
|
|
||
|
|
### Step 3: Integrate with Your Installation
|
||
|
|
|
||
|
|
Update your installation script to read from the catalog:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# scripts/install.sh
|
||
|
|
CATALOG=$(cat configs/services-catalog.toml)
|
||
|
|
PRESET=${1:-local}
|
||
|
|
|
||
|
|
# Extract services from preset
|
||
|
|
SERVICES=$(grep -A 10 "^\[preset.$PRESET.services\]" configs/services-catalog.toml)
|
||
|
|
|
||
|
|
# Install each enabled service
|
||
|
|
echo "$SERVICES" | while read line; do
|
||
|
|
if [[ $line == *"enabled = true"* ]]; then
|
||
|
|
SERVICE=$(echo $line | cut -d= -f1)
|
||
|
|
echo "Installing $SERVICE..."
|
||
|
|
# ... installation logic
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
```
|
||
|
|
|
||
|
|
### Step 4: Export for Provisioning
|
||
|
|
|
||
|
|
Create an export function that converts the catalog to YAML for provisioners:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# scripts/export-catalog.sh
|
||
|
|
PRESET=$1
|
||
|
|
|
||
|
|
cat > /tmp/export.yaml << EOF
|
||
|
|
preset: $PRESET
|
||
|
|
services:
|
||
|
|
EOF
|
||
|
|
|
||
|
|
grep -A 20 "^\[preset.$PRESET.services\]" configs/services-catalog.toml | \
|
||
|
|
while read line; do
|
||
|
|
if [[ $line == *"enabled = true"* ]]; then
|
||
|
|
echo " - $(echo $line | cut -d= -f1)" >> /tmp/export.yaml
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
|
||
|
|
cat /tmp/export.yaml
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Using with Provisioners
|
||
|
|
|
||
|
|
### Integration Pattern 1: Direct File Reading
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# provisioner reads catalog directly
|
||
|
|
provisioner \
|
||
|
|
--catalog myapp/configs/services-catalog.toml \
|
||
|
|
--preset production \
|
||
|
|
--output terraform/main.tf
|
||
|
|
```
|
||
|
|
|
||
|
|
### Integration Pattern 2: Export and Read
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# export from app
|
||
|
|
./scripts/export-catalog.sh production > /tmp/myapp-services.yaml
|
||
|
|
|
||
|
|
# provisioner reads export
|
||
|
|
provisioner \
|
||
|
|
--config /tmp/myapp-services.yaml \
|
||
|
|
--backend kubernetes \
|
||
|
|
--output k8s/deployment.yaml
|
||
|
|
```
|
||
|
|
|
||
|
|
### Integration Pattern 3: API/Bridge
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Create a simple HTTP bridge
|
||
|
|
python -m http.server --directory configs/
|
||
|
|
|
||
|
|
# Provisioner fetches
|
||
|
|
curl http://localhost:8000/services-catalog.toml
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Reusing in Other Applications
|
||
|
|
|
||
|
|
### Example 1: Microservices App
|
||
|
|
|
||
|
|
```toml
|
||
|
|
[service.auth-service]
|
||
|
|
name = "auth-service"
|
||
|
|
type = "server"
|
||
|
|
[service.auth-service.server]
|
||
|
|
default_port = 4000
|
||
|
|
|
||
|
|
[service.user-service]
|
||
|
|
name = "user-service"
|
||
|
|
type = "server"
|
||
|
|
[service.user-service.server]
|
||
|
|
default_port = 4001
|
||
|
|
|
||
|
|
[service.data-service]
|
||
|
|
name = "data-service"
|
||
|
|
type = "database"
|
||
|
|
[service.data-service.server]
|
||
|
|
default_port = 5432
|
||
|
|
|
||
|
|
[preset.dev]
|
||
|
|
[preset.dev.services]
|
||
|
|
auth_service = { enabled = true }
|
||
|
|
user_service = { enabled = true }
|
||
|
|
data_service = { enabled = true }
|
||
|
|
|
||
|
|
[preset.production]
|
||
|
|
[preset.production.services]
|
||
|
|
auth_service = { enabled = true, replicas = 3 }
|
||
|
|
user_service = { enabled = true, replicas = 2 }
|
||
|
|
data_service = { enabled = true, replicas = 1 }
|
||
|
|
```
|
||
|
|
|
||
|
|
### Example 2: Data Pipeline
|
||
|
|
|
||
|
|
```toml
|
||
|
|
[service.data-ingress]
|
||
|
|
name = "data-ingress"
|
||
|
|
type = "server"
|
||
|
|
[service.data-ingress.server]
|
||
|
|
default_port = 8000
|
||
|
|
|
||
|
|
[service.processor]
|
||
|
|
name = "processor"
|
||
|
|
type = "worker"
|
||
|
|
|
||
|
|
[service.storage]
|
||
|
|
name = "storage"
|
||
|
|
type = "database"
|
||
|
|
|
||
|
|
[preset.local-dev]
|
||
|
|
[preset.local-dev.services]
|
||
|
|
data_ingress = { enabled = true }
|
||
|
|
processor = { enabled = true }
|
||
|
|
storage = { enabled = true }
|
||
|
|
|
||
|
|
[preset.cloud]
|
||
|
|
[preset.cloud.services]
|
||
|
|
data_ingress = { enabled = true, scale = "auto" }
|
||
|
|
processor = { enabled = true, scale = "auto" }
|
||
|
|
storage = { enabled = true, backup = true }
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Key Design Principles
|
||
|
|
|
||
|
|
### 1. Configuration-First
|
||
|
|
|
||
|
|
Everything is configuration, no hardcoding:
|
||
|
|
- Service definitions in TOML
|
||
|
|
- Preset combinations in TOML
|
||
|
|
- Ports, health checks, dependencies all declarative
|
||
|
|
|
||
|
|
### 2. Single Source of Truth
|
||
|
|
|
||
|
|
One file (`services-catalog.toml`) defines everything:
|
||
|
|
- Local tools read it for discovery
|
||
|
|
- Remote provisioners read it for deployment
|
||
|
|
- No duplication between systems
|
||
|
|
|
||
|
|
### 3. Minimal Tooling Required
|
||
|
|
|
||
|
|
Works with standard tools:
|
||
|
|
- TOML is text-based (git-friendly)
|
||
|
|
- Can be read by bash, Python, Go, NuShell, etc.
|
||
|
|
- No special infrastructure needed
|
||
|
|
|
||
|
|
### 4. Self-Documenting
|
||
|
|
|
||
|
|
The catalog is both specification and documentation:
|
||
|
|
- Service definitions include examples
|
||
|
|
- Health checks documented
|
||
|
|
- Dependencies explicit
|
||
|
|
- Usage patterns clear
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Extending the Module
|
||
|
|
|
||
|
|
### Adding Custom Fields
|
||
|
|
|
||
|
|
You can extend the schema for your specific needs:
|
||
|
|
|
||
|
|
```toml
|
||
|
|
[service.my-service]
|
||
|
|
# Standard fields
|
||
|
|
name = "my-service"
|
||
|
|
type = "server"
|
||
|
|
|
||
|
|
# Custom fields for your app
|
||
|
|
custom_field_1 = "value"
|
||
|
|
tags = ["tag1", "tag2"]
|
||
|
|
compliance = "hipaa"
|
||
|
|
cost_per_month = 100
|
||
|
|
```
|
||
|
|
|
||
|
|
### Adding Custom Patterns
|
||
|
|
|
||
|
|
Create application-specific deployment patterns:
|
||
|
|
|
||
|
|
```toml
|
||
|
|
[pattern.ml-training]
|
||
|
|
name = "ML Training Pipeline"
|
||
|
|
description = "For batch ML training jobs"
|
||
|
|
services = ["data-ingress", "preprocessor", "trainer", "model-storage"]
|
||
|
|
|
||
|
|
[pattern.ml-serving]
|
||
|
|
name = "ML Model Serving"
|
||
|
|
description = "For ML inference in production"
|
||
|
|
services = ["model-service", "feature-store", "metrics-collector"]
|
||
|
|
```
|
||
|
|
|
||
|
|
### Adding Custom Validators
|
||
|
|
|
||
|
|
Extend validation to your needs:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# scripts/validate-catalog.sh
|
||
|
|
validate_compliance_tags() {
|
||
|
|
grep '^\[service\.' configs/services-catalog.toml | while read svc; do
|
||
|
|
if ! grep -A 10 "$svc" configs/services-catalog.toml | grep -q "compliance ="; then
|
||
|
|
echo "❌ $svc missing compliance tag"
|
||
|
|
fi
|
||
|
|
done
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Best Practices
|
||
|
|
|
||
|
|
### 1. Keep It Maintainable
|
||
|
|
|
||
|
|
- One service = one [service.xxx] block
|
||
|
|
- Group related fields together
|
||
|
|
- Use consistent naming
|
||
|
|
|
||
|
|
### 2. Document Services
|
||
|
|
|
||
|
|
```toml
|
||
|
|
[service.my-service]
|
||
|
|
# Always include usage examples
|
||
|
|
[service.my-service.usage]
|
||
|
|
examples = [
|
||
|
|
"Command to run locally",
|
||
|
|
"With config file: my-service --config config.toml",
|
||
|
|
"In Docker: docker run myapp/my-service",
|
||
|
|
]
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Version Your Catalog
|
||
|
|
|
||
|
|
```toml
|
||
|
|
[version]
|
||
|
|
catalog_version = "1.0.0" # schema version
|
||
|
|
app_version = "1.0.0" # your app version
|
||
|
|
updated_at = "2025-11-19"
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. Make It Testable
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# scripts/test-catalog.sh
|
||
|
|
test_valid_toml() {
|
||
|
|
nu -c "open configs/services-catalog.toml" > /dev/null
|
||
|
|
}
|
||
|
|
|
||
|
|
test_required_services() {
|
||
|
|
# Your app might require certain services
|
||
|
|
grep -q "app-api" configs/services-catalog.toml
|
||
|
|
}
|
||
|
|
|
||
|
|
test_valid_ports() {
|
||
|
|
# Check no duplicate ports
|
||
|
|
grep "default_port" configs/services-catalog.toml | sort | uniq -d | [ -z "$(cat)" ]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Integration Checklist
|
||
|
|
|
||
|
|
- [ ] Create `configs/services-catalog.toml`
|
||
|
|
- [ ] Define all services with metadata
|
||
|
|
- [ ] Define presets for different contexts
|
||
|
|
- [ ] Create simple discovery tool (bash/NuShell)
|
||
|
|
- [ ] Update installation script to read from catalog
|
||
|
|
- [ ] Add export function for provisioners
|
||
|
|
- [ ] Document how to extend the catalog
|
||
|
|
- [ ] Add validation tests
|
||
|
|
- [ ] Version control the catalog
|
||
|
|
- [ ] Document in your project README
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Real-World Example: Using with project-provisioning
|
||
|
|
|
||
|
|
### Step 1: Create Catalog (myapp/configs/)
|
||
|
|
```toml
|
||
|
|
[service.my-api]
|
||
|
|
display_name = "My API"
|
||
|
|
type = "server"
|
||
|
|
[service.my-api.server]
|
||
|
|
default_port = 3000
|
||
|
|
|
||
|
|
[preset.production]
|
||
|
|
[preset.production.services]
|
||
|
|
my_api = { enabled = true }
|
||
|
|
```
|
||
|
|
|
||
|
|
### Step 2: Export (myapp/scripts/)
|
||
|
|
```bash
|
||
|
|
#!/bin/bash
|
||
|
|
nu -c "open configs/services-catalog.toml | get preset | get production"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Step 3: Integrate (project-provisioning/scripts/)
|
||
|
|
```bash
|
||
|
|
#!/bin/bash
|
||
|
|
# Read from myapp
|
||
|
|
SERVICES=$(myapp/scripts/export-preset.sh production)
|
||
|
|
|
||
|
|
# Generate infrastructure
|
||
|
|
echo "$SERVICES" | while read service; do
|
||
|
|
# Create terraform resource
|
||
|
|
cat >> terraform/main.tf << EOF
|
||
|
|
resource "docker_container" "$service" {
|
||
|
|
name = "$service"
|
||
|
|
image = "myapp/$service:latest"
|
||
|
|
ports = [...] # extracted from catalog
|
||
|
|
}
|
||
|
|
EOF
|
||
|
|
done
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Conclusion
|
||
|
|
|
||
|
|
The Service Catalog abstraction is **language-agnostic** and **tool-agnostic**. It's a **simple but powerful pattern** for:
|
||
|
|
|
||
|
|
- Centralizing service definitions
|
||
|
|
- Enabling local discovery
|
||
|
|
- Integrating with remote provisioners
|
||
|
|
- Documenting infrastructure
|
||
|
|
- Maintaining single source of truth
|
||
|
|
|
||
|
|
Use it in any application where you need to manage multiple services or deployment contexts.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**Document Version**: 1.0.0
|
||
|
|
**Last Updated**: 2025-11-19
|
||
|
|
**Status**: Ready for implementation in external projects
|