mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-07 00:37:36 +00:00
680 lines
20 KiB
Go
680 lines
20 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/servicediscovery"
|
|
internalauth "github.com/rcourtman/pulse-go-rewrite/pkg/auth"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// AIConfigProvider provides access to the current AI configuration.
|
|
// This allows discovery handlers to show AI provider info without tight coupling.
|
|
type AIConfigProvider interface {
|
|
GetAIConfig() *config.AIConfig
|
|
}
|
|
|
|
// Note: adminBypassEnabled() is defined in auth.go
|
|
|
|
// DiscoveryHandlers handles AI-powered infrastructure discovery endpoints.
|
|
type DiscoveryHandlers struct {
|
|
service *servicediscovery.Service
|
|
config *config.Config // For admin status checks
|
|
aiConfigProvider AIConfigProvider
|
|
}
|
|
|
|
// NewDiscoveryHandlers creates new discovery handlers.
|
|
func NewDiscoveryHandlers(service *servicediscovery.Service, cfg *config.Config) *DiscoveryHandlers {
|
|
return &DiscoveryHandlers{
|
|
service: service,
|
|
config: cfg,
|
|
}
|
|
}
|
|
|
|
// SetService sets the discovery service (used for late initialization after routes are registered).
|
|
func (h *DiscoveryHandlers) SetService(service *servicediscovery.Service) {
|
|
h.service = service
|
|
}
|
|
|
|
// SetAIConfigProvider sets the AI config provider for showing AI provider info.
|
|
func (h *DiscoveryHandlers) SetAIConfigProvider(provider AIConfigProvider) {
|
|
h.aiConfigProvider = provider
|
|
}
|
|
|
|
// getAIProviderInfo returns info about the current AI provider for discovery.
|
|
func (h *DiscoveryHandlers) getAIProviderInfo() *servicediscovery.AIProviderInfo {
|
|
if h.aiConfigProvider == nil {
|
|
return nil
|
|
}
|
|
|
|
aiCfg := h.aiConfigProvider.GetAIConfig()
|
|
if aiCfg == nil || !aiCfg.Enabled {
|
|
return nil
|
|
}
|
|
|
|
// Get the discovery model
|
|
model := aiCfg.GetDiscoveryModel()
|
|
if model == "" {
|
|
return nil
|
|
}
|
|
|
|
// Parse the model to get provider
|
|
provider, modelName := config.ParseModelString(model)
|
|
|
|
// Determine if local
|
|
isLocal := provider == config.AIProviderOllama
|
|
|
|
// Build human-readable label
|
|
var label string
|
|
switch provider {
|
|
case config.AIProviderOllama:
|
|
label = "Local (Ollama)"
|
|
case config.AIProviderAnthropic:
|
|
label = "Cloud (Anthropic)"
|
|
case config.AIProviderOpenAI:
|
|
label = "Cloud (OpenAI)"
|
|
case config.AIProviderOpenRouter:
|
|
label = "Cloud (OpenRouter)"
|
|
case config.AIProviderDeepSeek:
|
|
label = "Cloud (DeepSeek)"
|
|
case config.AIProviderGemini:
|
|
label = "Cloud (Google Gemini)"
|
|
default:
|
|
label = "Cloud (" + provider + ")"
|
|
}
|
|
|
|
return &servicediscovery.AIProviderInfo{
|
|
Provider: provider,
|
|
Model: modelName,
|
|
IsLocal: isLocal,
|
|
Label: label,
|
|
}
|
|
}
|
|
|
|
// writeDiscoveryJSON writes a JSON response.
|
|
func writeDiscoveryJSON(w http.ResponseWriter, data any) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
json.NewEncoder(w).Encode(data)
|
|
}
|
|
|
|
// writeDiscoveryError writes a JSON error response.
|
|
func writeDiscoveryError(w http.ResponseWriter, statusCode int, message string) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(statusCode)
|
|
json.NewEncoder(w).Encode(map[string]any{
|
|
"error": true,
|
|
"message": message,
|
|
})
|
|
}
|
|
|
|
func parseDiscoveryResourceType(raw string) (servicediscovery.ResourceType, error) {
|
|
trimmed := strings.ToLower(strings.TrimSpace(raw))
|
|
if trimmed == "" {
|
|
return "", fmt.Errorf("resource type is required")
|
|
}
|
|
normalized := servicediscovery.NormalizeResourceType(servicediscovery.ResourceType(trimmed))
|
|
if !isSupportedDiscoveryResourceType(normalized) {
|
|
return "", fmt.Errorf("unsupported resource type %q", trimmed)
|
|
}
|
|
return normalized, nil
|
|
}
|
|
|
|
func isSupportedDiscoveryResourceType(resourceType servicediscovery.ResourceType) bool {
|
|
switch resourceType {
|
|
case servicediscovery.ResourceTypeVM,
|
|
servicediscovery.ResourceTypeSystemContainer,
|
|
servicediscovery.ResourceTypeDocker,
|
|
servicediscovery.ResourceTypeK8s,
|
|
servicediscovery.ResourceTypeAgent,
|
|
servicediscovery.ResourceTypeDockerVM,
|
|
servicediscovery.ResourceTypeDockerSystemContainer:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// isAdminRequest checks whether the request has privileged admin access for
|
|
// discovery secret operations. This is intentionally stricter than general
|
|
// authentication: session users must match configured admin identity and
|
|
// API tokens must include settings:write.
|
|
func (h *DiscoveryHandlers) isAdminRequest(r *http.Request) bool {
|
|
// Dev mode bypass - treat all requests as admin when enabled
|
|
if adminBypassEnabled() {
|
|
return true
|
|
}
|
|
|
|
if h.config == nil {
|
|
return false // Default to non-admin if no config
|
|
}
|
|
|
|
// 1. If using proxy auth, check the admin role
|
|
if h.config.ProxyAuthSecret != "" {
|
|
if valid, _, isAdmin := CheckProxyAuth(h.config, r); valid {
|
|
return isAdmin
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 2. Check for basic auth (Pulse single-user admin credential)
|
|
if username, password, ok := r.BasicAuth(); ok {
|
|
configuredUser := strings.TrimSpace(h.config.AuthUser)
|
|
configuredHash := strings.TrimSpace(h.config.AuthPass)
|
|
if configuredUser != "" && configuredHash != "" &&
|
|
username == configuredUser &&
|
|
internalauth.CheckPasswordHash(password, configuredHash) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// 3. Check for configured admin session (OIDC/SAML/local session)
|
|
if cookie, err := readSessionCookie(r); err == nil && cookie.Value != "" {
|
|
if ValidateSession(cookie.Value) {
|
|
configuredAdmin := strings.TrimSpace(h.config.AuthUser)
|
|
if configuredAdmin != "" {
|
|
sessionUser := strings.TrimSpace(GetSessionUsername(cookie.Value))
|
|
if strings.EqualFold(sessionUser, configuredAdmin) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Check API tokens; only settings:write tokens are admin-capable here.
|
|
if tokenRecord := getAPITokenRecordFromRequest(r); tokenRecord != nil {
|
|
return tokenRecord.HasScope(config.ScopeSettingsWrite)
|
|
}
|
|
validateTokenAsAdmin := func(raw string) bool {
|
|
if strings.TrimSpace(raw) == "" {
|
|
return false
|
|
}
|
|
config.Mu.Lock()
|
|
record, ok := h.config.ValidateAPIToken(raw)
|
|
config.Mu.Unlock()
|
|
return ok && record != nil && record.HasScope(config.ScopeSettingsWrite)
|
|
}
|
|
if token := strings.TrimSpace(r.Header.Get("X-API-Token")); token != "" {
|
|
if validateTokenAsAdmin(token) {
|
|
return true
|
|
}
|
|
}
|
|
if bearer := extractBearerToken(r.Header.Get("Authorization")); bearer != "" {
|
|
if validateTokenAsAdmin(bearer) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// redactSensitiveFields removes sensitive data from a discovery for non-admin users.
|
|
// This creates a copy to avoid modifying the original.
|
|
func redactSensitiveFields(d *servicediscovery.ResourceDiscovery) *servicediscovery.ResourceDiscovery {
|
|
if d == nil {
|
|
return nil
|
|
}
|
|
// Create a shallow copy
|
|
redacted := *d
|
|
// Redact sensitive fields
|
|
redacted.UserSecrets = nil // Never expose to non-admins
|
|
redacted.RawCommandOutput = nil // May contain sensitive output
|
|
return &redacted
|
|
}
|
|
|
|
func discoverySummaryResponse(d *servicediscovery.ResourceDiscovery) servicediscovery.DiscoverySummary {
|
|
if d == nil {
|
|
return servicediscovery.DiscoverySummary{}
|
|
}
|
|
return d.ToSummary()
|
|
}
|
|
|
|
func discoveryDetailResponse(d *servicediscovery.ResourceDiscovery) *servicediscovery.ResourceDiscovery {
|
|
return d
|
|
}
|
|
|
|
// HandleListDiscoveries handles GET /api/discovery
|
|
func (h *DiscoveryHandlers) HandleListDiscoveries(w http.ResponseWriter, r *http.Request) {
|
|
if h.service == nil {
|
|
writeDiscoveryError(w, http.StatusServiceUnavailable, "discovery service not configured")
|
|
return
|
|
}
|
|
|
|
discoveries, err := h.service.ListDiscoveries()
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to list discoveries")
|
|
writeDiscoveryError(w, http.StatusInternalServerError, "Failed to list discoveries")
|
|
return
|
|
}
|
|
|
|
// Convert to summaries for list view
|
|
summaries := make([]servicediscovery.DiscoverySummary, 0, len(discoveries))
|
|
for _, d := range discoveries {
|
|
if d == nil {
|
|
continue
|
|
}
|
|
summaries = append(summaries, discoverySummaryResponse(d))
|
|
}
|
|
|
|
writeDiscoveryJSON(w, map[string]any{
|
|
"discoveries": summaries,
|
|
"total": len(summaries),
|
|
})
|
|
}
|
|
|
|
// HandleGetDiscovery handles GET /api/discovery/{type}/{target}/{id}
|
|
func (h *DiscoveryHandlers) HandleGetDiscovery(w http.ResponseWriter, r *http.Request) {
|
|
if h.service == nil {
|
|
writeDiscoveryError(w, http.StatusServiceUnavailable, "discovery service not configured")
|
|
return
|
|
}
|
|
|
|
// Parse path: /api/discovery/{type}/{target}/{id}
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/discovery/")
|
|
parts := strings.SplitN(path, "/", 3)
|
|
if len(parts) < 3 {
|
|
writeDiscoveryError(w, http.StatusBadRequest, "Invalid path: expected /api/discovery/{type}/{target}/{id}")
|
|
return
|
|
}
|
|
|
|
resourceType, err := parseDiscoveryResourceType(parts[0])
|
|
if err != nil {
|
|
writeDiscoveryError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
targetID := parts[1]
|
|
resourceID := parts[2]
|
|
|
|
discovery, err := h.service.GetDiscoveryByResource(resourceType, targetID, resourceID)
|
|
if err != nil {
|
|
log.Error().Err(err).Str("type", string(resourceType)).Str("target", targetID).Str("id", resourceID).Msg("Failed to get discovery")
|
|
writeDiscoveryError(w, http.StatusInternalServerError, "Failed to get discovery")
|
|
return
|
|
}
|
|
|
|
if discovery == nil {
|
|
writeDiscoveryError(w, http.StatusNotFound, "Discovery not found")
|
|
return
|
|
}
|
|
|
|
// Redact sensitive fields for non-admin users
|
|
if !h.isAdminRequest(r) {
|
|
discovery = redactSensitiveFields(discovery)
|
|
}
|
|
writeDiscoveryJSON(w, discoveryDetailResponse(discovery))
|
|
}
|
|
|
|
// HandleTriggerDiscovery handles POST /api/discovery/{type}/{target}/{id}
|
|
func (h *DiscoveryHandlers) HandleTriggerDiscovery(w http.ResponseWriter, r *http.Request) {
|
|
if h.service == nil {
|
|
writeDiscoveryError(w, http.StatusServiceUnavailable, "discovery service not configured")
|
|
return
|
|
}
|
|
|
|
// Parse path
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/discovery/")
|
|
parts := strings.SplitN(path, "/", 3)
|
|
if len(parts) < 3 {
|
|
writeDiscoveryError(w, http.StatusBadRequest, "Invalid path: expected /api/discovery/{type}/{target}/{id}")
|
|
return
|
|
}
|
|
|
|
resourceType, err := parseDiscoveryResourceType(parts[0])
|
|
if err != nil {
|
|
writeDiscoveryError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
targetID := parts[1]
|
|
resourceID := parts[2]
|
|
|
|
// Parse optional request body for force flag and hostname
|
|
var reqBody struct {
|
|
Force bool `json:"force"`
|
|
Hostname string `json:"hostname"`
|
|
}
|
|
if r.Body != nil {
|
|
_ = json.NewDecoder(r.Body).Decode(&reqBody)
|
|
}
|
|
|
|
// Build discovery request
|
|
req := servicediscovery.DiscoveryRequest{
|
|
ResourceType: resourceType,
|
|
ResourceID: resourceID,
|
|
TargetID: targetID,
|
|
Hostname: reqBody.Hostname,
|
|
Force: reqBody.Force,
|
|
}
|
|
|
|
// If hostname not provided, use target ID as a fallback.
|
|
if req.Hostname == "" {
|
|
req.Hostname = targetID
|
|
}
|
|
|
|
discovery, err := h.service.DiscoverResource(r.Context(), req)
|
|
if err != nil {
|
|
log.Error().Err(err).
|
|
Str("type", string(resourceType)).
|
|
Str("target", targetID).
|
|
Str("id", resourceID).
|
|
Msg("Failed to trigger discovery")
|
|
writeDiscoveryError(w, http.StatusInternalServerError, "Discovery failed: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Redact sensitive fields for non-admin users
|
|
if !h.isAdminRequest(r) {
|
|
discovery = redactSensitiveFields(discovery)
|
|
}
|
|
writeDiscoveryJSON(w, discoveryDetailResponse(discovery))
|
|
}
|
|
|
|
// HandleUpdateNotes handles PUT /api/discovery/{type}/{target}/{id}/notes
|
|
func (h *DiscoveryHandlers) HandleUpdateNotes(w http.ResponseWriter, r *http.Request) {
|
|
if h.service == nil {
|
|
writeDiscoveryError(w, http.StatusServiceUnavailable, "discovery service not configured")
|
|
return
|
|
}
|
|
|
|
// Parse path
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/discovery/")
|
|
path = strings.TrimSuffix(path, "/notes")
|
|
parts := strings.SplitN(path, "/", 3)
|
|
if len(parts) < 3 {
|
|
writeDiscoveryError(w, http.StatusBadRequest, "Invalid path")
|
|
return
|
|
}
|
|
|
|
resourceType, err := parseDiscoveryResourceType(parts[0])
|
|
if err != nil {
|
|
writeDiscoveryError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
targetID := parts[1]
|
|
resourceID := parts[2]
|
|
|
|
// Build the full ID
|
|
discoveryID := servicediscovery.MakeResourceID(resourceType, targetID, resourceID)
|
|
|
|
// Parse request body
|
|
var req servicediscovery.UpdateNotesRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeDiscoveryError(w, http.StatusBadRequest, "Invalid request body")
|
|
return
|
|
}
|
|
|
|
// Only admins can set user_secrets (contains sensitive data like API tokens)
|
|
isAdmin := h.isAdminRequest(r)
|
|
if !isAdmin && len(req.UserSecrets) > 0 {
|
|
writeDiscoveryError(w, http.StatusForbidden, "Only admins can set user_secrets")
|
|
return
|
|
}
|
|
|
|
if err := h.service.UpdateNotes(discoveryID, req.UserNotes, req.UserSecrets); err != nil {
|
|
log.Error().Err(err).Str("id", discoveryID).Msg("Failed to update notes")
|
|
writeDiscoveryError(w, http.StatusInternalServerError, "Failed to update notes: "+err.Error())
|
|
return
|
|
}
|
|
|
|
// Return updated discovery
|
|
discovery, err := h.service.GetDiscovery(discoveryID)
|
|
if err != nil {
|
|
writeDiscoveryError(w, http.StatusInternalServerError, "Notes updated but failed to fetch result")
|
|
return
|
|
}
|
|
|
|
// Redact sensitive fields for non-admin users
|
|
if !isAdmin {
|
|
discovery = redactSensitiveFields(discovery)
|
|
}
|
|
writeDiscoveryJSON(w, discoveryDetailResponse(discovery))
|
|
}
|
|
|
|
// HandleDeleteDiscovery handles DELETE /api/discovery/{type}/{target}/{id}
|
|
func (h *DiscoveryHandlers) HandleDeleteDiscovery(w http.ResponseWriter, r *http.Request) {
|
|
if h.service == nil {
|
|
writeDiscoveryError(w, http.StatusServiceUnavailable, "discovery service not configured")
|
|
return
|
|
}
|
|
|
|
// Parse path
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/discovery/")
|
|
parts := strings.SplitN(path, "/", 3)
|
|
if len(parts) < 3 {
|
|
writeDiscoveryError(w, http.StatusBadRequest, "Invalid path")
|
|
return
|
|
}
|
|
|
|
resourceType, err := parseDiscoveryResourceType(parts[0])
|
|
if err != nil {
|
|
writeDiscoveryError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
targetID := parts[1]
|
|
resourceID := parts[2]
|
|
|
|
discoveryID := servicediscovery.MakeResourceID(resourceType, targetID, resourceID)
|
|
|
|
if err := h.service.DeleteDiscovery(discoveryID); err != nil {
|
|
log.Error().Err(err).Str("id", discoveryID).Msg("Failed to delete discovery")
|
|
writeDiscoveryError(w, http.StatusInternalServerError, "Failed to delete discovery")
|
|
return
|
|
}
|
|
|
|
writeDiscoveryJSON(w, map[string]any{"success": true, "id": discoveryID})
|
|
}
|
|
|
|
// HandleGetProgress handles GET /api/discovery/{type}/{target}/{id}/progress
|
|
func (h *DiscoveryHandlers) HandleGetProgress(w http.ResponseWriter, r *http.Request) {
|
|
if h.service == nil {
|
|
writeDiscoveryError(w, http.StatusServiceUnavailable, "discovery service not configured")
|
|
return
|
|
}
|
|
|
|
// Parse path
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/discovery/")
|
|
path = strings.TrimSuffix(path, "/progress")
|
|
parts := strings.SplitN(path, "/", 3)
|
|
if len(parts) < 3 {
|
|
writeDiscoveryError(w, http.StatusBadRequest, "Invalid path")
|
|
return
|
|
}
|
|
|
|
resourceType, err := parseDiscoveryResourceType(parts[0])
|
|
if err != nil {
|
|
writeDiscoveryError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
targetID := parts[1]
|
|
resourceID := parts[2]
|
|
|
|
discoveryID := servicediscovery.MakeResourceID(resourceType, targetID, resourceID)
|
|
|
|
progress := h.service.GetProgress(discoveryID)
|
|
if progress == nil {
|
|
// Not currently scanning - check if we have a discovery
|
|
discovery, err := h.service.GetDiscovery(discoveryID)
|
|
if err == nil && discovery != nil {
|
|
// Return completed status with all fields for frontend compatibility
|
|
writeDiscoveryJSON(w, map[string]any{
|
|
"resource_id": discoveryID,
|
|
"status": "completed",
|
|
"current_step": "",
|
|
"total_steps": 0,
|
|
"completed_steps": 0,
|
|
"started_at": discovery.DiscoveredAt,
|
|
"updated_at": discovery.UpdatedAt,
|
|
})
|
|
return
|
|
}
|
|
|
|
// Return not_started status with all fields for frontend compatibility
|
|
writeDiscoveryJSON(w, map[string]any{
|
|
"resource_id": discoveryID,
|
|
"status": "not_started",
|
|
"current_step": "",
|
|
"total_steps": 0,
|
|
"completed_steps": 0,
|
|
"started_at": "",
|
|
})
|
|
return
|
|
}
|
|
|
|
writeDiscoveryJSON(w, progress)
|
|
}
|
|
|
|
// HandleGetStatus handles GET /api/discovery/status
|
|
func (h *DiscoveryHandlers) HandleGetStatus(w http.ResponseWriter, r *http.Request) {
|
|
if h.service == nil {
|
|
writeDiscoveryError(w, http.StatusServiceUnavailable, "discovery service not configured")
|
|
return
|
|
}
|
|
|
|
status := h.service.GetStatus()
|
|
|
|
// Add fingerprint change/stale stats
|
|
changedCount, _ := h.service.GetChangedResourceCount()
|
|
staleCount, _ := h.service.GetStaleResourceCount()
|
|
|
|
status["changed_count"] = changedCount // Containers with changed fingerprints
|
|
status["stale_count"] = staleCount // Discoveries > 30 days old
|
|
|
|
writeDiscoveryJSON(w, status)
|
|
}
|
|
|
|
// HandleUpdateSettings handles PUT /api/discovery/settings
|
|
// Allows updating discovery settings like the staleness threshold.
|
|
func (h *DiscoveryHandlers) HandleUpdateSettings(w http.ResponseWriter, r *http.Request) {
|
|
if h.service == nil {
|
|
writeDiscoveryError(w, http.StatusServiceUnavailable, "discovery service not configured")
|
|
return
|
|
}
|
|
|
|
// Require admin privileges
|
|
if !h.isAdminRequest(r) {
|
|
writeDiscoveryError(w, http.StatusForbidden, "Admin privileges required")
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
MaxDiscoveryAgeDays int `json:"max_discovery_age_days"` // Days before rediscovery
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeDiscoveryError(w, http.StatusBadRequest, "Invalid request body")
|
|
return
|
|
}
|
|
|
|
// Update settings
|
|
if req.MaxDiscoveryAgeDays > 0 {
|
|
h.service.SetMaxDiscoveryAge(time.Duration(req.MaxDiscoveryAgeDays) * 24 * time.Hour)
|
|
log.Info().Int("days", req.MaxDiscoveryAgeDays).Msg("Max discovery age updated via API")
|
|
}
|
|
|
|
// Return updated status
|
|
status := h.service.GetStatus()
|
|
changedCount, _ := h.service.GetChangedResourceCount()
|
|
staleCount, _ := h.service.GetStaleResourceCount()
|
|
status["changed_count"] = changedCount
|
|
status["stale_count"] = staleCount
|
|
|
|
writeDiscoveryJSON(w, status)
|
|
}
|
|
|
|
// HandleListByType handles GET /api/discovery/type/{type}
|
|
func (h *DiscoveryHandlers) HandleListByType(w http.ResponseWriter, r *http.Request) {
|
|
if h.service == nil {
|
|
writeDiscoveryError(w, http.StatusServiceUnavailable, "discovery service not configured")
|
|
return
|
|
}
|
|
|
|
// Parse path
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/discovery/type/")
|
|
resourceType, err := parseDiscoveryResourceType(path)
|
|
if err != nil {
|
|
writeDiscoveryError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
discoveries, err := h.service.ListDiscoveriesByType(resourceType)
|
|
if err != nil {
|
|
log.Error().Err(err).Str("type", string(resourceType)).Msg("Failed to list discoveries by type")
|
|
writeDiscoveryError(w, http.StatusInternalServerError, "Failed to list discoveries")
|
|
return
|
|
}
|
|
|
|
summaries := make([]servicediscovery.DiscoverySummary, 0, len(discoveries))
|
|
for _, d := range discoveries {
|
|
if d == nil {
|
|
continue
|
|
}
|
|
summaries = append(summaries, discoverySummaryResponse(d))
|
|
}
|
|
|
|
writeDiscoveryJSON(w, map[string]any{
|
|
"discoveries": summaries,
|
|
"total": len(summaries),
|
|
"type": resourceType,
|
|
})
|
|
}
|
|
|
|
// HandleListByAgent handles GET /api/discovery/agent/{agentId}
|
|
func (h *DiscoveryHandlers) HandleListByAgent(w http.ResponseWriter, r *http.Request) {
|
|
if h.service == nil {
|
|
writeDiscoveryError(w, http.StatusServiceUnavailable, "discovery service not configured")
|
|
return
|
|
}
|
|
|
|
// Parse path
|
|
agentID := strings.TrimPrefix(r.URL.Path, "/api/discovery/agent/")
|
|
|
|
discoveries, err := h.service.ListDiscoveriesByTarget(agentID)
|
|
if err != nil {
|
|
log.Error().Err(err).Str("agentId", agentID).Msg("Failed to list discoveries by agent")
|
|
writeDiscoveryError(w, http.StatusInternalServerError, "Failed to list discoveries")
|
|
return
|
|
}
|
|
|
|
summaries := make([]servicediscovery.DiscoverySummary, 0, len(discoveries))
|
|
for _, d := range discoveries {
|
|
if d == nil {
|
|
continue
|
|
}
|
|
summaries = append(summaries, discoverySummaryResponse(d))
|
|
}
|
|
|
|
writeDiscoveryJSON(w, map[string]any{
|
|
"discoveries": summaries,
|
|
"total": len(summaries),
|
|
"agentId": agentID,
|
|
})
|
|
}
|
|
|
|
// HandleGetInfo handles GET /api/discovery/info/{type}
|
|
// Returns metadata about the discovery process: AI provider info and commands that will run.
|
|
func (h *DiscoveryHandlers) HandleGetInfo(w http.ResponseWriter, r *http.Request) {
|
|
// Parse resource type from path
|
|
path := strings.TrimPrefix(r.URL.Path, "/api/discovery/info/")
|
|
resourceType, err := parseDiscoveryResourceType(path)
|
|
if err != nil {
|
|
writeDiscoveryError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
|
|
// Get commands for this resource type
|
|
commands := servicediscovery.GetCommandsForResource(resourceType)
|
|
categories := servicediscovery.GetCommandCategories(resourceType)
|
|
|
|
// Get AI provider info
|
|
aiProvider := h.getAIProviderInfo()
|
|
|
|
info := servicediscovery.DiscoveryInfo{
|
|
AIProvider: aiProvider,
|
|
Commands: commands,
|
|
CommandCategories: categories,
|
|
}
|
|
|
|
writeDiscoveryJSON(w, info)
|
|
}
|