Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 28 additions & 17 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,26 +26,37 @@ VAULT_TOKEN=
# Or leave empty and it will be read from ~/.config/vault/root-token

# ===========================================================================
# Docker Network IP Addresses (172.20.0.0/16)
# Docker Network IP Addresses (4-Tier Segmentation)
# ===========================================================================
# Static IP addresses for services in the dev-services network
# Static IP addresses for services across 4 network segments:
# - vault-network: 172.20.1.0/24 (secrets management)
# - data-network: 172.20.2.0/24 (databases, redis, rabbitmq)
# - app-network: 172.20.3.0/24 (forgejo, reference APIs)
# - observability-network: 172.20.4.0/24 (prometheus, grafana, loki)
#
# Change these if you need custom IP assignments or have conflicts
#
# Default assignments:
POSTGRES_IP=172.20.0.10
PGBOUNCER_IP=172.20.0.11
MYSQL_IP=172.20.0.12
REDIS_1_IP=172.20.0.13
RABBITMQ_IP=172.20.0.14
MONGODB_IP=172.20.0.15
REDIS_2_IP=172.20.0.16
REDIS_3_IP=172.20.0.17
FORGEJO_IP=172.20.0.20
VAULT_IP=172.20.0.21
REFERENCE_API_IP=172.20.0.100
PROMETHEUS_IP=172.20.0.101
GRAFANA_IP=172.20.0.102
LOKI_IP=172.20.0.103
# Vault Network (172.20.1.x):
VAULT_IP=172.20.1.5

# Data Network (172.20.2.x):
POSTGRES_IP=172.20.2.10
PGBOUNCER_IP=172.20.2.11
MYSQL_IP=172.20.2.12
REDIS_1_IP=172.20.2.13
RABBITMQ_IP=172.20.2.14
MONGODB_IP=172.20.2.15
REDIS_2_IP=172.20.2.16
REDIS_3_IP=172.20.2.17

# App Network (172.20.3.x):
FORGEJO_IP=172.20.3.20
REFERENCE_API_IP=172.20.3.100

# Observability Network (172.20.4.x):
PROMETHEUS_IP=172.20.4.10
GRAFANA_IP=172.20.4.20
LOKI_IP=172.20.4.30

# ===========================================================================
# TLS Configuration (Enabled by Default)
Expand Down
51 changes: 51 additions & 0 deletions docs/VAULT.md
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,57 @@ vault write -f -field=secret_id auth/approle/role/reference-api/secret-id > ~/.c
- AppRole implementation: `reference-apps/fastapi/app/services/vault.py`
- Docker configuration: `docker-compose.yml` (line 879)

#### AppRole Secret ID Renewal

**⚠️ IMPORTANT:** AppRole secret_ids have a **30-day TTL**. Services will fail to authenticate after expiry.

**Check Secret ID Expiry:**
```bash
export VAULT_ADDR=http://localhost:8200
export VAULT_TOKEN=$(cat ~/.config/vault/root-token)

# Check remaining TTL for a service (e.g., reference-api)
vault write auth/approle/role/reference-api/secret-id-accessor/lookup \
secret_id_accessor=$(cat ~/.config/vault/approles/reference-api/secret-id-accessor 2>/dev/null)
```

**Renew Secret IDs (Before Expiry):**
```bash
# Renew all AppRole secret_ids
./scripts/vault-approle-bootstrap.sh --renew-secrets

# Or renew for a specific service
SERVICE=reference-api
vault write -f -field=secret_id auth/approle/role/${SERVICE}/secret-id \
> ~/.config/vault/approles/${SERVICE}/secret-id

# Save the accessor for future lookups
vault write -f -field=secret_id_accessor auth/approle/role/${SERVICE}/secret-id \
> ~/.config/vault/approles/${SERVICE}/secret-id-accessor

# Restart the service to pick up new credentials
docker compose restart ${SERVICE}
```

**Automated Renewal (Recommended for Production):**

Add to crontab to renew secret_ids weekly:
```bash
# Edit crontab
crontab -e

# Add this line (runs every Sunday at 3 AM)
0 3 * * 0 cd /path/to/devstack-core && ./scripts/vault-approle-bootstrap.sh --renew-secrets >> /var/log/vault-approle-renewal.log 2>&1
```

**Renewal Timeline:**
| Event | TTL Remaining | Action |
|-------|---------------|--------|
| Created | 30 days | Normal operation |
| Week 3 | 7-14 days | Consider renewal |
| Week 4 | < 7 days | **Renew immediately** |
| Expired | 0 days | Service authentication fails |

### SSL/TLS Certificate Management

**TLS Implementation: Pre-Generated Certificates with Vault-Based Configuration**
Expand Down
83 changes: 66 additions & 17 deletions reference-apps/fastapi-api-first/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
from slowapi.errors import RateLimitExceeded
from prometheus_client import Counter, Histogram, Gauge, generate_latest, CONTENT_TYPE_LATEST
import logging
import sys
import time
import uuid
from pythonjsonlogger import jsonlogger

from app.routers import (
health_checks,
Expand All @@ -30,12 +32,19 @@
from app.middleware.cache import cache_manager
from app.services.vault import vault_client

# Configure logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
# Configure structured JSON logging (matches code-first implementation)
logHandler = logging.StreamHandler(sys.stdout)
formatter = jsonlogger.JsonFormatter(
'%(asctime)s %(name)s %(levelname)s %(message)s %(request_id)s %(method)s %(path)s %(status_code)s %(duration_ms)s'
)
logHandler.setFormatter(formatter)
logger = logging.getLogger(__name__)
logger.addHandler(logHandler)
logger.setLevel(logging.INFO)

# Disable default basicConfig
logging.getLogger().handlers.clear()
logging.getLogger().addHandler(logHandler)

# Prometheus metrics
http_requests_total = Counter(
Expand Down Expand Up @@ -68,7 +77,7 @@
# Create FastAPI app
app = FastAPI(
title="DevStack Core - Reference API (API-First)",
version="1.0.0",
version="1.1.0",
description="API-First implementation generated from OpenAPI specification",
docs_url="/docs",
redoc_url="/redoc",
Expand Down Expand Up @@ -112,10 +121,10 @@ async def metrics_middleware(request: Request, call_next):
request.state.request_id = request_id

method = request.method
path = request.url.path
endpoint = request.url.path

# Track in-progress requests
http_requests_in_progress.labels(method=method, endpoint=path).inc()
http_requests_in_progress.labels(method=method, endpoint=endpoint).inc()

start_time = time.time()

Expand All @@ -126,23 +135,58 @@ async def metrics_middleware(request: Request, call_next):
# Record metrics
http_requests_total.labels(
method=method,
endpoint=path,
endpoint=endpoint,
status=response.status_code
).inc()

http_request_duration_seconds.labels(
method=method,
endpoint=path
endpoint=endpoint
).observe(duration)

# Log request with structured data (matches code-first implementation)
logger.info(
"HTTP request completed",
extra={
"request_id": request_id,
"method": method,
"path": endpoint,
"status_code": response.status_code,
"duration_ms": round(duration * 1000, 2)
}
)

# Add headers
response.headers["X-Request-ID"] = request_id
response.headers["X-Response-Time"] = f"{duration:.3f}s"

return response

except Exception as e:
# Record error metrics
duration = time.time() - start_time
http_requests_total.labels(
method=method,
endpoint=endpoint,
status=500
).inc()

# Log error with structured data
logger.error(
f"Request failed: {str(e)}",
extra={
"request_id": request_id,
"method": method,
"path": endpoint,
"status_code": 500,
"duration_ms": round(duration * 1000, 2)
},
exc_info=True
)
raise

finally:
http_requests_in_progress.labels(method=method, endpoint=path).dec()
http_requests_in_progress.labels(method=method, endpoint=endpoint).dec()


# Include routers
Expand All @@ -158,7 +202,7 @@ async def metrics_middleware(request: Request, call_next):
async def startup_event():
"""Application startup event handler."""
# Set app info metric
app_info.labels(version="1.0.0", name="api-first").set(1)
app_info.labels(version="1.1.0", name="api-first").set(1)

# Initialize response caching with Redis
try:
Expand All @@ -171,10 +215,14 @@ async def startup_event():
logger.error(f"Failed to initialize cache: {e}")
logger.warning("Application will continue without caching")

logger.info("Starting API-First FastAPI application...")
logger.info(f"Debug mode: {settings.DEBUG}")
logger.info(f"Vault address: {settings.VAULT_ADDR}")
logger.info(f"Redis cache enabled: {cache_manager.enabled}")
logger.info(
"Starting DevStack Core Reference API (API-First)",
extra={
"vault_address": settings.VAULT_ADDR,
"redis_cache_enabled": cache_manager.enabled,
"version": "1.1.0"
}
)
logger.info("Application ready")


Expand All @@ -195,7 +243,7 @@ async def root(request: Request):
"""
return {
"name": "DevStack Core Reference API",
"version": "1.0.0",
"version": "1.1.0",
"description": "Reference implementation for infrastructure integration",
"docs": "/docs",
"health": "/health/all",
Expand Down Expand Up @@ -253,7 +301,8 @@ async def root(request: Request):


@app.get("/metrics")
async def metrics():
@limiter.limit("1000/minute") # High limit for metrics scraping
async def metrics(request: Request):
"""Prometheus metrics endpoint"""
return Response(
content=generate_latest(),
Expand Down
2 changes: 1 addition & 1 deletion reference-apps/fastapi/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ python-json-logger==4.0.0
prometheus-client==0.23.1

# Testing
pytest==9.0.1 # pytest-asyncio 1.2.0 requires pytest<9
pytest==9.0.1
pytest-asyncio==1.3.0
pytest-cov==7.0.0
pytest-mock==3.15.1
Expand Down
5 changes: 5 additions & 0 deletions reference-apps/golang/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/google/uuid v1.6.0
github.com/hashicorp/vault/api v1.22.0
github.com/jackc/pgx/v5 v5.7.6
github.com/prometheus/client_golang v1.19.1
github.com/rabbitmq/amqp091-go v1.10.0
github.com/redis/go-redis/v9 v9.17.2
github.com/sirupsen/logrus v1.9.3
Expand All @@ -16,6 +17,7 @@ require (

require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
Expand Down Expand Up @@ -53,6 +55,9 @@ require (
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/montanaflynn/stats v0.7.1 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.48.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.1 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
Expand Down
10 changes: 10 additions & 0 deletions reference-apps/golang/go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
Expand Down Expand Up @@ -110,6 +112,14 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg=
Expand Down
2 changes: 1 addition & 1 deletion reference-apps/golang/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func Load() *Config {
return &Config{
// Application
Environment: getEnv("ENVIRONMENT", "development"),
Debug: getEnvBool("DEBUG", true),
Debug: getEnvBool("DEBUG", false),
HTTPPort: getEnv("HTTP_PORT", "8002"),
HTTPSPort: getEnv("HTTPS_PORT", "8445"),
EnableTLS: getEnvBool("GOLANG_API_ENABLE_TLS", false),
Expand Down
22 changes: 20 additions & 2 deletions reference-apps/nodejs/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,25 @@
/**
* Jest Configuration
*/

module.exports = {
testEnvironment: 'node',
coverageDirectory: 'coverage',
collectCoverageFrom: ['src/**/*.js'],
testMatch: ['**/tests/**/*.test.js'],
collectCoverageFrom: [
'src/**/*.js',
'!src/index.js' // Exclude main entry point from coverage
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html'],
coverageThreshold: {
global: {
branches: 50,
functions: 50,
lines: 50,
statements: 50
}
},
setupFilesAfterEnv: ['./tests/setup.js'],
testTimeout: 10000,
verbose: true
};
Loading
Loading