forked from cerc-io/stack-orchestrator
- AI-FRIENDLY-PLAN.md: Plan for making repo AI-friendly - STACK-CREATION-GUIDE.md: Implementation details for create-stack command - laconic-network-deployment.md: Laconic network deployment overview Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
414 lines
11 KiB
Markdown
414 lines
11 KiB
Markdown
# Implementing `laconic-so create-stack` Command
|
|
|
|
A plan for adding a new CLI command to scaffold stack files automatically.
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
Add a `create-stack` command that generates all required files for a new stack:
|
|
|
|
```bash
|
|
laconic-so create-stack --name my-stack --type webapp
|
|
```
|
|
|
|
**Output:**
|
|
```
|
|
stack_orchestrator/data/
|
|
├── stacks/my-stack/stack.yml
|
|
├── container-build/cerc-my-stack/
|
|
│ ├── Dockerfile
|
|
│ └── build.sh
|
|
└── compose/docker-compose-my-stack.yml
|
|
|
|
Updated: repository-list.txt, container-image-list.txt, pod-list.txt
|
|
```
|
|
|
|
---
|
|
|
|
## CLI Architecture Summary
|
|
|
|
### Command Registration Pattern
|
|
|
|
Commands are Click functions registered in `main.py`:
|
|
|
|
```python
|
|
# main.py (line ~70)
|
|
from stack_orchestrator.create import create_stack
|
|
cli.add_command(create_stack.command, "create-stack")
|
|
```
|
|
|
|
### Global Options Access
|
|
|
|
```python
|
|
from stack_orchestrator.opts import opts
|
|
|
|
if not opts.o.quiet:
|
|
print("message")
|
|
if opts.o.dry_run:
|
|
print("(would create files)")
|
|
```
|
|
|
|
### Key Utilities
|
|
|
|
| Function | Location | Purpose |
|
|
|----------|----------|---------|
|
|
| `get_yaml()` | `util.py` | YAML parser (ruamel.yaml) |
|
|
| `get_stack_path(stack)` | `util.py` | Resolve stack directory path |
|
|
| `error_exit(msg)` | `util.py` | Print error and exit(1) |
|
|
|
|
---
|
|
|
|
## Files to Create
|
|
|
|
### 1. Command Module
|
|
|
|
**`stack_orchestrator/create/__init__.py`**
|
|
```python
|
|
# Empty file to make this a package
|
|
```
|
|
|
|
**`stack_orchestrator/create/create_stack.py`**
|
|
```python
|
|
import click
|
|
import os
|
|
from pathlib import Path
|
|
from shutil import copy
|
|
from stack_orchestrator.opts import opts
|
|
from stack_orchestrator.util import error_exit, get_yaml
|
|
|
|
# Template types
|
|
STACK_TEMPLATES = {
|
|
"webapp": {
|
|
"description": "Web application with Node.js",
|
|
"base_image": "node:20-bullseye-slim",
|
|
"port": 3000,
|
|
},
|
|
"service": {
|
|
"description": "Backend service",
|
|
"base_image": "python:3.11-slim",
|
|
"port": 8080,
|
|
},
|
|
"empty": {
|
|
"description": "Minimal stack with no defaults",
|
|
"base_image": None,
|
|
"port": None,
|
|
},
|
|
}
|
|
|
|
|
|
def get_data_dir() -> Path:
|
|
"""Get path to stack_orchestrator/data directory"""
|
|
return Path(__file__).absolute().parent.parent.joinpath("data")
|
|
|
|
|
|
def validate_stack_name(name: str) -> None:
|
|
"""Validate stack name follows conventions"""
|
|
import re
|
|
if not re.match(r'^[a-z0-9][a-z0-9-]*[a-z0-9]$', name) and len(name) > 2:
|
|
error_exit(f"Invalid stack name '{name}'. Use lowercase alphanumeric with hyphens.")
|
|
if name.startswith("cerc-"):
|
|
error_exit("Stack name should not start with 'cerc-' (container names will add this prefix)")
|
|
|
|
|
|
def create_stack_yml(stack_dir: Path, name: str, template: dict, repo_url: str) -> None:
|
|
"""Create stack.yml file"""
|
|
config = {
|
|
"version": "1.2",
|
|
"name": name,
|
|
"description": template.get("description", f"Stack: {name}"),
|
|
"repos": [repo_url] if repo_url else [],
|
|
"containers": [f"cerc/{name}"],
|
|
"pods": [name],
|
|
}
|
|
|
|
stack_dir.mkdir(parents=True, exist_ok=True)
|
|
with open(stack_dir / "stack.yml", "w") as f:
|
|
get_yaml().dump(config, f)
|
|
|
|
|
|
def create_dockerfile(container_dir: Path, name: str, template: dict) -> None:
|
|
"""Create Dockerfile"""
|
|
base_image = template.get("base_image", "node:20-bullseye-slim")
|
|
port = template.get("port", 3000)
|
|
|
|
dockerfile_content = f'''# Build stage
|
|
FROM {base_image} AS builder
|
|
|
|
WORKDIR /app
|
|
COPY package*.json ./
|
|
RUN npm ci
|
|
COPY . .
|
|
RUN npm run build
|
|
|
|
# Production stage
|
|
FROM {base_image}
|
|
|
|
WORKDIR /app
|
|
COPY package*.json ./
|
|
RUN npm ci --only=production
|
|
COPY --from=builder /app/dist ./dist
|
|
|
|
EXPOSE {port}
|
|
CMD ["npm", "run", "start"]
|
|
'''
|
|
|
|
container_dir.mkdir(parents=True, exist_ok=True)
|
|
with open(container_dir / "Dockerfile", "w") as f:
|
|
f.write(dockerfile_content)
|
|
|
|
|
|
def create_build_script(container_dir: Path, name: str) -> None:
|
|
"""Create build.sh script"""
|
|
build_script = f'''#!/usr/bin/env bash
|
|
# Build cerc/{name}
|
|
|
|
source ${{CERC_CONTAINER_BASE_DIR}}/build-base.sh
|
|
|
|
SCRIPT_DIR=$( cd -- "$( dirname -- "${{BASH_SOURCE[0]}}" )" &> /dev/null && pwd )
|
|
|
|
docker build -t cerc/{name}:local \\
|
|
-f ${{SCRIPT_DIR}}/Dockerfile \\
|
|
${{build_command_args}} \\
|
|
${{CERC_REPO_BASE_DIR}}/{name}
|
|
'''
|
|
|
|
build_path = container_dir / "build.sh"
|
|
with open(build_path, "w") as f:
|
|
f.write(build_script)
|
|
|
|
# Make executable
|
|
os.chmod(build_path, 0o755)
|
|
|
|
|
|
def create_compose_file(compose_dir: Path, name: str, template: dict) -> None:
|
|
"""Create docker-compose file"""
|
|
port = template.get("port", 3000)
|
|
|
|
compose_content = {
|
|
"version": "3.8",
|
|
"services": {
|
|
name: {
|
|
"image": f"cerc/{name}:local",
|
|
"restart": "unless-stopped",
|
|
"ports": [f"${{HOST_PORT:-{port}}}:{port}"],
|
|
"environment": {
|
|
"NODE_ENV": "${NODE_ENV:-production}",
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
with open(compose_dir / f"docker-compose-{name}.yml", "w") as f:
|
|
get_yaml().dump(compose_content, f)
|
|
|
|
|
|
def update_list_file(data_dir: Path, filename: str, entry: str) -> None:
|
|
"""Add entry to a list file if not already present"""
|
|
list_path = data_dir / filename
|
|
|
|
# Read existing entries
|
|
existing = set()
|
|
if list_path.exists():
|
|
with open(list_path, "r") as f:
|
|
existing = set(line.strip() for line in f if line.strip())
|
|
|
|
# Add new entry
|
|
if entry not in existing:
|
|
with open(list_path, "a") as f:
|
|
f.write(f"{entry}\n")
|
|
|
|
|
|
@click.command()
|
|
@click.option("--name", required=True, help="Name of the new stack (lowercase, hyphens)")
|
|
@click.option("--type", "stack_type", default="webapp",
|
|
type=click.Choice(list(STACK_TEMPLATES.keys())),
|
|
help="Stack template type")
|
|
@click.option("--repo", help="Git repository URL (e.g., github.com/org/repo)")
|
|
@click.option("--force", is_flag=True, help="Overwrite existing files")
|
|
@click.pass_context
|
|
def command(ctx, name: str, stack_type: str, repo: str, force: bool):
|
|
"""Create a new stack with all required files.
|
|
|
|
Examples:
|
|
|
|
laconic-so create-stack --name my-app --type webapp
|
|
|
|
laconic-so create-stack --name my-service --type service --repo github.com/org/repo
|
|
"""
|
|
# Validate
|
|
validate_stack_name(name)
|
|
|
|
template = STACK_TEMPLATES[stack_type]
|
|
data_dir = get_data_dir()
|
|
|
|
# Define paths
|
|
stack_dir = data_dir / "stacks" / name
|
|
container_dir = data_dir / "container-build" / f"cerc-{name}"
|
|
compose_dir = data_dir / "compose"
|
|
|
|
# Check for existing files
|
|
if not force:
|
|
if stack_dir.exists():
|
|
error_exit(f"Stack already exists: {stack_dir}\nUse --force to overwrite")
|
|
if container_dir.exists():
|
|
error_exit(f"Container build dir exists: {container_dir}\nUse --force to overwrite")
|
|
|
|
# Dry run check
|
|
if opts.o.dry_run:
|
|
print(f"Would create stack '{name}' with template '{stack_type}':")
|
|
print(f" - {stack_dir}/stack.yml")
|
|
print(f" - {container_dir}/Dockerfile")
|
|
print(f" - {container_dir}/build.sh")
|
|
print(f" - {compose_dir}/docker-compose-{name}.yml")
|
|
print(f" - Update repository-list.txt")
|
|
print(f" - Update container-image-list.txt")
|
|
print(f" - Update pod-list.txt")
|
|
return
|
|
|
|
# Create files
|
|
if not opts.o.quiet:
|
|
print(f"Creating stack '{name}' with template '{stack_type}'...")
|
|
|
|
create_stack_yml(stack_dir, name, template, repo)
|
|
if opts.o.verbose:
|
|
print(f" Created {stack_dir}/stack.yml")
|
|
|
|
create_dockerfile(container_dir, name, template)
|
|
if opts.o.verbose:
|
|
print(f" Created {container_dir}/Dockerfile")
|
|
|
|
create_build_script(container_dir, name)
|
|
if opts.o.verbose:
|
|
print(f" Created {container_dir}/build.sh")
|
|
|
|
create_compose_file(compose_dir, name, template)
|
|
if opts.o.verbose:
|
|
print(f" Created {compose_dir}/docker-compose-{name}.yml")
|
|
|
|
# Update list files
|
|
if repo:
|
|
update_list_file(data_dir, "repository-list.txt", repo)
|
|
if opts.o.verbose:
|
|
print(f" Added {repo} to repository-list.txt")
|
|
|
|
update_list_file(data_dir, "container-image-list.txt", f"cerc/{name}")
|
|
if opts.o.verbose:
|
|
print(f" Added cerc/{name} to container-image-list.txt")
|
|
|
|
update_list_file(data_dir, "pod-list.txt", name)
|
|
if opts.o.verbose:
|
|
print(f" Added {name} to pod-list.txt")
|
|
|
|
# Summary
|
|
if not opts.o.quiet:
|
|
print(f"\nStack '{name}' created successfully!")
|
|
print(f"\nNext steps:")
|
|
print(f" 1. Edit {stack_dir}/stack.yml")
|
|
print(f" 2. Customize {container_dir}/Dockerfile")
|
|
print(f" 3. Run: laconic-so --stack {name} build-containers")
|
|
print(f" 4. Run: laconic-so --stack {name} deploy-system up")
|
|
```
|
|
|
|
### 2. Register Command in main.py
|
|
|
|
**Edit `stack_orchestrator/main.py`**
|
|
|
|
Add import:
|
|
```python
|
|
from stack_orchestrator.create import create_stack
|
|
```
|
|
|
|
Add command registration (after line ~78):
|
|
```python
|
|
cli.add_command(create_stack.command, "create-stack")
|
|
```
|
|
|
|
---
|
|
|
|
## Implementation Steps
|
|
|
|
### Step 1: Create module structure
|
|
```bash
|
|
mkdir -p stack_orchestrator/create
|
|
touch stack_orchestrator/create/__init__.py
|
|
```
|
|
|
|
### Step 2: Create the command file
|
|
Create `stack_orchestrator/create/create_stack.py` with the code above.
|
|
|
|
### Step 3: Register in main.py
|
|
Add the import and `cli.add_command()` line.
|
|
|
|
### Step 4: Test the command
|
|
```bash
|
|
# Show help
|
|
laconic-so create-stack --help
|
|
|
|
# Dry run
|
|
laconic-so --dry-run create-stack --name test-app --type webapp
|
|
|
|
# Create a stack
|
|
laconic-so create-stack --name test-app --type webapp --repo github.com/org/test-app
|
|
|
|
# Verify
|
|
ls -la stack_orchestrator/data/stacks/test-app/
|
|
cat stack_orchestrator/data/stacks/test-app/stack.yml
|
|
```
|
|
|
|
---
|
|
|
|
## Template Types
|
|
|
|
| Type | Base Image | Port | Use Case |
|
|
|------|------------|------|----------|
|
|
| `webapp` | node:20-bullseye-slim | 3000 | React/Vue/Next.js apps |
|
|
| `service` | python:3.11-slim | 8080 | Python backend services |
|
|
| `empty` | none | none | Custom from scratch |
|
|
|
|
---
|
|
|
|
## Future Enhancements
|
|
|
|
1. **Interactive mode** - Prompt for values if not provided
|
|
2. **More templates** - Go, Rust, database stacks
|
|
3. **Template from existing** - `--from-stack existing-stack`
|
|
4. **External stack support** - Create in custom directory
|
|
5. **Validation command** - `laconic-so validate-stack --name my-stack`
|
|
|
|
---
|
|
|
|
## Files Modified
|
|
|
|
| File | Change |
|
|
|------|--------|
|
|
| `stack_orchestrator/create/__init__.py` | New (empty) |
|
|
| `stack_orchestrator/create/create_stack.py` | New (command implementation) |
|
|
| `stack_orchestrator/main.py` | Add import and `cli.add_command()` |
|
|
|
|
---
|
|
|
|
## Verification
|
|
|
|
```bash
|
|
# 1. Command appears in help
|
|
laconic-so --help | grep create-stack
|
|
|
|
# 2. Dry run works
|
|
laconic-so --dry-run create-stack --name verify-test --type webapp
|
|
|
|
# 3. Full creation works
|
|
laconic-so create-stack --name verify-test --type webapp
|
|
ls stack_orchestrator/data/stacks/verify-test/
|
|
ls stack_orchestrator/data/container-build/cerc-verify-test/
|
|
ls stack_orchestrator/data/compose/docker-compose-verify-test.yml
|
|
|
|
# 4. Build works
|
|
laconic-so --stack verify-test build-containers
|
|
|
|
# 5. Cleanup
|
|
rm -rf stack_orchestrator/data/stacks/verify-test
|
|
rm -rf stack_orchestrator/data/container-build/cerc-verify-test
|
|
rm stack_orchestrator/data/compose/docker-compose-verify-test.yml
|
|
```
|