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>
176 lines
4.6 KiB
Go
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"},
|
|
},
|
|
}
|
|
}
|