CI / changes (push) Successful in 6s
CI / commitlint (push) Skipped
CI / openapi (push) Successful in 27s
CI / web (push) Successful in 51s
CI / go (push) Successful in 2m19s
CI / bird2 (push) Successful in 13s
CI / release (push) Successful in 4m24s
Added support for portal JWT authentication, enabling single sign-on (SSO) capabilities. Updated the application to handle JWT claims for user permissions and roles, enhancing security and access control. Refactored relevant components and API routes to accommodate the new authentication flow, ensuring a seamless user experience. Updated documentation to reflect the new authentication requirements and configurations. Co-authored-by: Cursor <cursoragent@cursor.com>
255 lines
7.8 KiB
Go
255 lines
7.8 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"evobgp/internal/runtimelogs"
|
|
"evobgp/internal/store"
|
|
)
|
|
|
|
func (s *Server) registerRuntimeLogsRoutes(m *http.ServeMux) {
|
|
m.HandleFunc("GET /runtime-logs/files", s.handleListRuntimeLogFiles)
|
|
m.HandleFunc("GET /runtime-logs/files/{filename}", s.handleGetRuntimeLogTail)
|
|
m.HandleFunc("DELETE /runtime-logs/files/{filename}", s.handleDeleteRuntimeLogFile)
|
|
m.HandleFunc("GET /runtime-logs/cleanup-audit", s.handleListRuntimeLogCleanupAudit)
|
|
m.HandleFunc("GET /runtime-logs/auto-estimate", s.handleRuntimeLogAutoEstimate)
|
|
m.HandleFunc("POST /runtime-logs/auto-run", s.handleRuntimeLogAutoRun)
|
|
}
|
|
|
|
func (s *Server) requireRuntimeLogs(w http.ResponseWriter) bool {
|
|
if s.runtimeLogs != nil && s.runtimeLogs.Available() {
|
|
return true
|
|
}
|
|
writeProblem(w, http.StatusServiceUnavailable, "Unavailable", "runtime_logs_unavailable")
|
|
return false
|
|
}
|
|
|
|
func writeRuntimeLogsErr(w http.ResponseWriter, operation string, err error) {
|
|
switch {
|
|
case errors.Is(err, runtimelogs.ErrUnavailable):
|
|
writeProblem(w, http.StatusServiceUnavailable, "Unavailable", "runtime_logs_unavailable")
|
|
case errors.Is(err, runtimelogs.ErrNotFound):
|
|
writeProblem(w, http.StatusNotFound, "Not Found", notFoundDetail)
|
|
case errors.Is(err, runtimelogs.ErrFileTooLarge):
|
|
writeProblem(w, http.StatusRequestEntityTooLarge, "Payload Too Large", "file exceeds maximum size for cleanup")
|
|
case errors.Is(err, runtimelogs.ErrInvalidFilename), errors.Is(err, runtimelogs.ErrNotAFile):
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", invalidInputDetail)
|
|
default:
|
|
writeInternalError(w, operation, err)
|
|
}
|
|
}
|
|
|
|
func runtimeLogFileJSON(f store.RuntimeLogFile) map[string]any {
|
|
return map[string]any{
|
|
"name": f.Name,
|
|
"size_bytes": f.SizeBytes,
|
|
"modified_at": f.ModifiedAt.UTC().Format("2006-01-02T15:04:05Z"),
|
|
}
|
|
}
|
|
|
|
func runtimeLogCleanupAuditJSON(row *store.RuntimeLogCleanupAudit) map[string]any {
|
|
out := map[string]any{
|
|
"id": row.ID,
|
|
"tenant_id": row.TenantID,
|
|
"actor_prefix": row.ActorPrefix,
|
|
"filename": row.Filename,
|
|
"action": row.Action,
|
|
"size_before": row.SizeBefore,
|
|
"created_at": row.CreatedAt.UTC().Format("2006-01-02T15:04:05Z"),
|
|
}
|
|
if row.SizeAfter != nil {
|
|
out["size_after"] = *row.SizeAfter
|
|
}
|
|
if row.Detail != nil {
|
|
out["detail"] = row.Detail
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (s *Server) handleListRuntimeLogFiles(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requirePerm(w, a, "bgp:monitoring:read") || !s.requireRuntimeLogs(w) {
|
|
return
|
|
}
|
|
items, err := s.runtimeLogs.ListFiles()
|
|
if err != nil {
|
|
writeRuntimeLogsErr(w, "runtime_logs_list", err)
|
|
return
|
|
}
|
|
out := make([]map[string]any, 0, len(items))
|
|
for _, f := range items {
|
|
out = append(out, runtimeLogFileJSON(f))
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": out})
|
|
}
|
|
|
|
func (s *Server) handleGetRuntimeLogTail(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requirePerm(w, a, "bgp:monitoring:read") || !s.requireRuntimeLogs(w) {
|
|
return
|
|
}
|
|
filename := r.PathValue("filename")
|
|
opts := runtimelogs.TailOptions{
|
|
Lines: parsePositiveIntQuery(r, "lines", runtimelogs.DefaultTailLines, runtimelogs.MaxTailLines),
|
|
Bytes: parsePositiveIntQuery(r, "bytes", 0, runtimelogs.MaxTailBytes),
|
|
Grep: r.URL.Query().Get("grep"),
|
|
}
|
|
tail, err := s.runtimeLogs.Tail(filename, opts)
|
|
if err != nil {
|
|
writeRuntimeLogsErr(w, "runtime_logs_tail", err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"filename": tail.Filename,
|
|
"content": tail.Content,
|
|
"truncated": tail.Truncated,
|
|
"lines_returned": tail.LinesReturned,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleDeleteRuntimeLogFile(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requirePerm(w, a, "bgp:tenant_settings:admin") || !s.requireRuntimeLogs(w) {
|
|
return
|
|
}
|
|
filename := r.PathValue("filename")
|
|
mode := r.URL.Query().Get("mode")
|
|
if mode == "" {
|
|
mode = store.RuntimeLogCleanupTruncate
|
|
}
|
|
if !store.ValidRuntimeLogCleanupAction(mode) {
|
|
writeProblem(w, http.StatusUnprocessableEntity, "Unprocessable Entity", invalidInputDetail)
|
|
return
|
|
}
|
|
sizeBefore, sizeAfter, err := s.runtimeLogs.Cleanup(filename, mode)
|
|
if err != nil {
|
|
writeRuntimeLogsErr(w, "runtime_logs_cleanup", err)
|
|
return
|
|
}
|
|
auditID, err := s.store.AppendRuntimeLogCleanupAudit(
|
|
a.TenantID, actorPrefix(a), filename, mode, sizeBefore, sizeAfter, nil)
|
|
if err != nil {
|
|
writeInternalError(w, "runtime_logs_cleanup_audit", err)
|
|
return
|
|
}
|
|
out := map[string]any{
|
|
"audit_id": auditID,
|
|
"filename": filename,
|
|
"action": mode,
|
|
"size_before": sizeBefore,
|
|
}
|
|
if sizeAfter != nil {
|
|
out["size_after"] = *sizeAfter
|
|
}
|
|
writeJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
func (s *Server) runtimeLogAutoPolicy(w http.ResponseWriter, r *http.Request, tenantID string) (runtimelogs.AutoPolicy, bool) {
|
|
settings, err := s.store.ListGlobalSettings(tenantID)
|
|
if err != nil {
|
|
writeInternalError(w, "runtime_logs_policy", err)
|
|
return runtimelogs.AutoPolicy{}, false
|
|
}
|
|
return runtimelogs.PolicyFromSettings(settings), true
|
|
}
|
|
|
|
func (s *Server) handleRuntimeLogAutoEstimate(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requirePerm(w, a, "bgp:tenant_settings:admin") || !s.requireRuntimeLogs(w) {
|
|
return
|
|
}
|
|
policy, ok := s.runtimeLogAutoPolicy(w, r, a.TenantID)
|
|
if !ok {
|
|
return
|
|
}
|
|
items, err := runtimelogs.EstimateAutoCleanup(s.runtimeLogs, policy)
|
|
if err != nil {
|
|
writeRuntimeLogsErr(w, "runtime_logs_auto_estimate", err)
|
|
return
|
|
}
|
|
out := make([]map[string]any, 0, len(items))
|
|
var wouldCount int
|
|
for _, it := range items {
|
|
if it.WouldCleanup {
|
|
wouldCount++
|
|
}
|
|
row := map[string]any{
|
|
"filename": it.Filename,
|
|
"size_bytes": it.SizeBytes,
|
|
"would_cleanup": it.WouldCleanup,
|
|
}
|
|
if it.SkipReason != "" {
|
|
row["skip_reason"] = it.SkipReason
|
|
}
|
|
out = append(out, row)
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
"policy": map[string]any{
|
|
"enabled": policy.Enabled,
|
|
"max_file_bytes": policy.MaxFileBytes,
|
|
"schedule": policy.Schedule,
|
|
"mode": policy.Mode,
|
|
},
|
|
"items": out,
|
|
"would_count": wouldCount,
|
|
})
|
|
}
|
|
|
|
func (s *Server) handleRuntimeLogAutoRun(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requirePerm(w, a, "bgp:tenant_settings:admin") || !s.requireRuntimeLogs(w) {
|
|
return
|
|
}
|
|
policy, ok := s.runtimeLogAutoPolicy(w, r, a.TenantID)
|
|
if !ok {
|
|
return
|
|
}
|
|
dryRun := r.URL.Query().Get("dry_run") == "true"
|
|
result, err := runtimelogs.RunAutoCleanup(r.Context(), runtimelogs.AutoCleanupDeps{
|
|
Service: s.runtimeLogs,
|
|
Store: s.store,
|
|
TenantID: a.TenantID,
|
|
}, policy, dryRun, "manual")
|
|
if err != nil {
|
|
writeRuntimeLogsErr(w, "runtime_logs_auto_run", err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, result)
|
|
}
|
|
|
|
func (s *Server) handleListRuntimeLogCleanupAudit(w http.ResponseWriter, r *http.Request) {
|
|
a, ok := authFromContext(r.Context())
|
|
if !ok || !s.requirePerm(w, a, "bgp:monitoring:read") {
|
|
return
|
|
}
|
|
cursor := r.URL.Query().Get("cursor")
|
|
limit := parseLimitQuery(r, 20, 100)
|
|
items, next, hasMore, err := s.store.ListRuntimeLogCleanupAudit(a.TenantID, cursor, limit)
|
|
if err != nil {
|
|
writeInternalError(w, "runtime_logs_cleanup_audit_list", err)
|
|
return
|
|
}
|
|
out := make([]map[string]any, 0, len(items))
|
|
for _, row := range items {
|
|
out = append(out, runtimeLogCleanupAuditJSON(row))
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]any{"items": out, "next_cursor": next, "has_more": hasMore})
|
|
}
|
|
|
|
func parsePositiveIntQuery(r *http.Request, key string, def, max int) int {
|
|
v := r.URL.Query().Get(key)
|
|
if v == "" {
|
|
return def
|
|
}
|
|
n, err := strconv.Atoi(v)
|
|
if err != nil || n <= 0 {
|
|
return def
|
|
}
|
|
if max > 0 && n > max {
|
|
return max
|
|
}
|
|
return n
|
|
}
|