rdev/pkg/api/openapi.go
jordan 72d16929ca feat: Implement hexagonal architecture with services, webhooks, queue, and telemetry
Major refactoring to hexagonal (ports & adapters) architecture:

- Add service layer (apikey_service, project_service) for business logic
- Add webhook system with dispatcher and delivery tracking
- Add command queue with priority-based processing
- Add rate limiting with sliding window algorithm
- Add audit logging for command execution
- Add OpenTelemetry integration (traces, metrics, spans)
- Add circuit breaker for fault tolerance
- Add cached repository wrapper for performance
- Add comprehensive validation package
- Add Kubernetes client integration for pod management
- Add database migrations (allowed_ips, audit_log, rate_limiting, queue, webhooks)
- Add network policy and PodDisruptionBudget for k8s
- Remove legacy executor and projects/registry packages
- Untrack secrets.yaml (now managed via envault)
- Add coverage.out to .gitignore
- Add e2e test infrastructure with docker-compose
- Add comprehensive documentation (API, architecture, operations, plans)
- Add golangci-lint config and pre-commit hook

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:57:46 -07:00

176 lines
4.6 KiB
Go

package api
import (
"encoding/json"
"fmt"
"net/http"
"sync"
scalargo "github.com/bdpiprava/scalar-go"
)
// OpenAPIInfo contains metadata about the API.
type OpenAPIInfo struct {
Title string `json:"title"`
Description string `json:"description,omitempty"`
Version string `json:"version"`
}
// OpenAPIServer describes a server endpoint.
type OpenAPIServer struct {
URL string `json:"url"`
Description string `json:"description,omitempty"`
}
// OpenAPISpec represents a minimal OpenAPI 3.0 specification.
type OpenAPISpec struct {
OpenAPI string `json:"openapi"`
Info OpenAPIInfo `json:"info"`
Servers []OpenAPIServer `json:"servers,omitempty"`
Paths map[string]map[string]interface{} `json:"paths"`
Tags []OpenAPITag `json:"tags,omitempty"`
mu sync.RWMutex
}
// OpenAPITag groups operations together.
type OpenAPITag struct {
Name string `json:"name"`
Description string `json:"description,omitempty"`
}
// NewOpenAPISpec creates a new OpenAPI specification builder.
func NewOpenAPISpec(title, version string) *OpenAPISpec {
return &OpenAPISpec{
OpenAPI: "3.0.3",
Info: OpenAPIInfo{
Title: title,
Version: version,
},
Paths: make(map[string]map[string]interface{}),
}
}
// WithDescription sets the API description.
func (s *OpenAPISpec) WithDescription(desc string) *OpenAPISpec {
s.Info.Description = desc
return s
}
// WithServer adds a server to the spec.
func (s *OpenAPISpec) WithServer(url, description string) *OpenAPISpec {
s.Servers = append(s.Servers, OpenAPIServer{
URL: url,
Description: description,
})
return s
}
// WithTag adds a tag for grouping operations.
func (s *OpenAPISpec) WithTag(name, description string) *OpenAPISpec {
s.Tags = append(s.Tags, OpenAPITag{
Name: name,
Description: description,
})
return s
}
// AddPath adds an operation to the spec.
// method should be lowercase (get, post, put, patch, delete).
func (s *OpenAPISpec) AddPath(path, method string, operation map[string]interface{}) *OpenAPISpec {
s.mu.Lock()
defer s.mu.Unlock()
if s.Paths[path] == nil {
s.Paths[path] = make(map[string]interface{})
}
s.Paths[path][method] = operation
return s
}
// JSON returns the spec as JSON bytes.
func (s *OpenAPISpec) JSON() ([]byte, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return json.MarshalIndent(s, "", " ")
}
// EnableDocs adds /docs and /openapi.json endpoints to the app.
func (a *App) EnableDocs(spec *OpenAPISpec) {
// Serve OpenAPI JSON
a.router.Get("/openapi.json", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Access-Control-Allow-Origin", "*")
specBytes, err := spec.JSON()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
_, _ = w.Write(specBytes)
})
// Serve Scalar docs UI
a.router.Get("/docs", func(w http.ResponseWriter, r *http.Request) {
// Detect scheme: check X-Forwarded-Proto first (for reverse proxy/TLS termination),
// then fall back to r.TLS for direct HTTPS connections
scheme := "http"
if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
scheme = proto
} else if r.TLS != nil {
scheme = "https"
}
specURL := fmt.Sprintf("%s://%s/openapi.json", scheme, r.Host)
html, err := scalargo.NewV2(
scalargo.WithSpecURL(specURL),
scalargo.WithDarkMode(),
)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = fmt.Fprint(w, html)
})
a.logger.Info("API documentation enabled", "docs", "/docs", "spec", "/openapi.json")
}
// Op creates an OpenAPI operation helper.
func Op(summary, description string, tags ...string) map[string]any {
return map[string]any{
"summary": summary,
"description": description,
"tags": tags,
"responses": map[string]any{
"200": map[string]any{"description": "Success"},
},
}
}
// OpWithBody creates an OpenAPI operation with a request body.
func OpWithBody(summary, description string, tags ...string) map[string]any {
return map[string]any{
"summary": summary,
"description": description,
"tags": tags,
"requestBody": map[string]any{
"required": true,
"content": map[string]any{
"application/json": map[string]any{
"schema": map[string]any{
"type": "object",
},
},
},
},
"responses": map[string]any{
"200": map[string]any{"description": "Success"},
"201": map[string]any{"description": "Created"},
},
}
}