mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-29 03:50:18 +00:00
1073 lines
29 KiB
Go
1073 lines
29 KiB
Go
package tools
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
)
|
|
|
|
// StateGetter provides access to the current infrastructure state
|
|
type StateGetter interface {
|
|
GetState() models.StateSnapshot
|
|
}
|
|
|
|
// AlertManagerMCPAdapter adapts alerts.Manager to MCP AlertProvider interface
|
|
type AlertManagerMCPAdapter struct {
|
|
manager AlertManager
|
|
}
|
|
|
|
// AlertManager interface matches what alerts.Manager provides
|
|
type AlertManager interface {
|
|
GetActiveAlerts() []alerts.Alert
|
|
}
|
|
|
|
// NewAlertManagerMCPAdapter creates a new adapter for alert manager
|
|
func NewAlertManagerMCPAdapter(manager AlertManager) *AlertManagerMCPAdapter {
|
|
if manager == nil {
|
|
return nil
|
|
}
|
|
return &AlertManagerMCPAdapter{manager: manager}
|
|
}
|
|
|
|
// GetActiveAlerts implements mcp.AlertProvider
|
|
func (a *AlertManagerMCPAdapter) GetActiveAlerts() []ActiveAlert {
|
|
if a.manager == nil {
|
|
return nil
|
|
}
|
|
|
|
activeAlerts := a.manager.GetActiveAlerts()
|
|
result := make([]ActiveAlert, 0, len(activeAlerts))
|
|
|
|
for _, alert := range activeAlerts {
|
|
result = append(result, ActiveAlert{
|
|
ID: alert.ID,
|
|
ResourceID: alert.ResourceID,
|
|
ResourceName: alert.ResourceName,
|
|
Type: alert.Type,
|
|
Severity: string(alert.Level),
|
|
Value: alert.Value,
|
|
Threshold: alert.Threshold,
|
|
StartTime: alert.StartTime,
|
|
Message: alert.Message,
|
|
})
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// StorageMCPAdapter adapts the monitor state to MCP StorageProvider interface
|
|
type StorageMCPAdapter struct {
|
|
stateGetter StateGetter
|
|
}
|
|
|
|
// NewStorageMCPAdapter creates a new adapter for storage data
|
|
func NewStorageMCPAdapter(stateGetter StateGetter) *StorageMCPAdapter {
|
|
if stateGetter == nil {
|
|
return nil
|
|
}
|
|
return &StorageMCPAdapter{stateGetter: stateGetter}
|
|
}
|
|
|
|
// GetStorage implements mcp.StorageProvider
|
|
func (a *StorageMCPAdapter) GetStorage() []models.Storage {
|
|
if a.stateGetter == nil {
|
|
return nil
|
|
}
|
|
state := a.stateGetter.GetState()
|
|
return state.Storage
|
|
}
|
|
|
|
// GetCephClusters implements mcp.StorageProvider
|
|
func (a *StorageMCPAdapter) GetCephClusters() []models.CephCluster {
|
|
if a.stateGetter == nil {
|
|
return nil
|
|
}
|
|
state := a.stateGetter.GetState()
|
|
return state.CephClusters
|
|
}
|
|
|
|
// GuestConfigSource provides guest configuration data with context.
|
|
type GuestConfigSource interface {
|
|
GetGuestConfig(ctx context.Context, guestType, instance, node string, vmid int) (map[string]interface{}, error)
|
|
}
|
|
|
|
// GuestConfigMCPAdapter adapts monitoring config access to MCP GuestConfigProvider interface.
|
|
type GuestConfigMCPAdapter struct {
|
|
source GuestConfigSource
|
|
timeout time.Duration
|
|
}
|
|
|
|
// NewGuestConfigMCPAdapter creates a new adapter for guest config data.
|
|
func NewGuestConfigMCPAdapter(source GuestConfigSource) *GuestConfigMCPAdapter {
|
|
if source == nil {
|
|
return nil
|
|
}
|
|
return &GuestConfigMCPAdapter{
|
|
source: source,
|
|
timeout: 5 * time.Second,
|
|
}
|
|
}
|
|
|
|
// GetGuestConfig implements mcp.GuestConfigProvider.
|
|
func (a *GuestConfigMCPAdapter) GetGuestConfig(guestType, instance, node string, vmid int) (map[string]interface{}, error) {
|
|
if a == nil || a.source == nil {
|
|
return nil, fmt.Errorf("guest config source not available")
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), a.timeout)
|
|
defer cancel()
|
|
return a.source.GetGuestConfig(ctx, guestType, instance, node, vmid)
|
|
}
|
|
|
|
// BackupMCPAdapter adapts the monitor state to MCP BackupProvider interface
|
|
type BackupMCPAdapter struct {
|
|
stateGetter StateGetter
|
|
}
|
|
|
|
// NewBackupMCPAdapter creates a new adapter for backup data
|
|
func NewBackupMCPAdapter(stateGetter StateGetter) *BackupMCPAdapter {
|
|
if stateGetter == nil {
|
|
return nil
|
|
}
|
|
return &BackupMCPAdapter{stateGetter: stateGetter}
|
|
}
|
|
|
|
// GetBackups implements mcp.BackupProvider
|
|
func (a *BackupMCPAdapter) GetBackups() models.Backups {
|
|
if a.stateGetter == nil {
|
|
return models.Backups{}
|
|
}
|
|
state := a.stateGetter.GetState()
|
|
return state.Backups
|
|
}
|
|
|
|
// GetPBSInstances implements mcp.BackupProvider
|
|
func (a *BackupMCPAdapter) GetPBSInstances() []models.PBSInstance {
|
|
if a.stateGetter == nil {
|
|
return nil
|
|
}
|
|
state := a.stateGetter.GetState()
|
|
return state.PBSInstances
|
|
}
|
|
|
|
// DiskHealthMCPAdapter adapts the monitor state to MCP DiskHealthProvider interface
|
|
type DiskHealthMCPAdapter struct {
|
|
stateGetter StateGetter
|
|
}
|
|
|
|
// NewDiskHealthMCPAdapter creates a new adapter for disk health data
|
|
func NewDiskHealthMCPAdapter(stateGetter StateGetter) *DiskHealthMCPAdapter {
|
|
if stateGetter == nil {
|
|
return nil
|
|
}
|
|
return &DiskHealthMCPAdapter{stateGetter: stateGetter}
|
|
}
|
|
|
|
// GetHosts implements mcp.DiskHealthProvider
|
|
func (a *DiskHealthMCPAdapter) GetHosts() []models.Host {
|
|
if a.stateGetter == nil {
|
|
return nil
|
|
}
|
|
state := a.stateGetter.GetState()
|
|
return state.Hosts
|
|
}
|
|
|
|
// RawMetricPoint represents a single metric value at a point in time
|
|
type RawMetricPoint struct {
|
|
Value float64
|
|
Timestamp time.Time
|
|
}
|
|
|
|
// MetricsSource provides access to historical metrics data
|
|
type MetricsSource interface {
|
|
GetGuestMetrics(guestID string, metricType string, duration time.Duration) []RawMetricPoint
|
|
GetNodeMetrics(nodeID string, metricType string, duration time.Duration) []RawMetricPoint
|
|
GetAllGuestMetrics(guestID string, duration time.Duration) map[string][]RawMetricPoint
|
|
}
|
|
|
|
// MetricsHistoryMCPAdapter adapts the metrics history to MCP MetricsHistoryProvider interface
|
|
type MetricsHistoryMCPAdapter struct {
|
|
stateGetter StateGetter
|
|
metricsSource MetricsSource
|
|
}
|
|
|
|
// NewMetricsHistoryMCPAdapter creates a new adapter for metrics history
|
|
func NewMetricsHistoryMCPAdapter(stateGetter StateGetter, metricsSource MetricsSource) *MetricsHistoryMCPAdapter {
|
|
if stateGetter == nil || metricsSource == nil {
|
|
return nil
|
|
}
|
|
return &MetricsHistoryMCPAdapter{
|
|
stateGetter: stateGetter,
|
|
metricsSource: metricsSource,
|
|
}
|
|
}
|
|
|
|
// GetResourceMetrics implements mcp.MetricsHistoryProvider
|
|
func (a *MetricsHistoryMCPAdapter) GetResourceMetrics(resourceID string, period time.Duration) ([]MetricPoint, error) {
|
|
if a.metricsSource == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
// Try guest metrics first (VMs and containers)
|
|
allMetrics := a.metricsSource.GetAllGuestMetrics(resourceID, period)
|
|
if len(allMetrics) == 0 {
|
|
// Try node metrics
|
|
cpuPoints := a.metricsSource.GetNodeMetrics(resourceID, "cpu", period)
|
|
memPoints := a.metricsSource.GetNodeMetrics(resourceID, "memory", period)
|
|
if len(cpuPoints) > 0 || len(memPoints) > 0 {
|
|
allMetrics = map[string][]RawMetricPoint{
|
|
"cpu": cpuPoints,
|
|
"memory": memPoints,
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(allMetrics) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Merge metrics by timestamp
|
|
return mergeMetricsByTimestamp(allMetrics), nil
|
|
}
|
|
|
|
// GetAllMetricsSummary implements mcp.MetricsHistoryProvider
|
|
func (a *MetricsHistoryMCPAdapter) GetAllMetricsSummary(period time.Duration) (map[string]ResourceMetricsSummary, error) {
|
|
if a.stateGetter == nil || a.metricsSource == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
state := a.stateGetter.GetState()
|
|
result := make(map[string]ResourceMetricsSummary)
|
|
|
|
// Process VMs
|
|
for _, vm := range state.VMs {
|
|
vmID := fmt.Sprintf("%d", vm.VMID)
|
|
if summary := a.computeSummary(vmID, vm.Name, "vm", period); summary != nil {
|
|
result[vmID] = *summary
|
|
}
|
|
}
|
|
|
|
// Process containers
|
|
for _, ct := range state.Containers {
|
|
ctID := fmt.Sprintf("%d", ct.VMID)
|
|
if summary := a.computeSummary(ctID, ct.Name, "container", period); summary != nil {
|
|
result[ctID] = *summary
|
|
}
|
|
}
|
|
|
|
// Process nodes
|
|
for _, node := range state.Nodes {
|
|
if summary := a.computeSummary(node.ID, node.Name, "node", period); summary != nil {
|
|
result[node.ID] = *summary
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (a *MetricsHistoryMCPAdapter) computeSummary(resourceID, resourceName, resourceType string, period time.Duration) *ResourceMetricsSummary {
|
|
var cpuPoints, memPoints []RawMetricPoint
|
|
|
|
if resourceType == "node" {
|
|
cpuPoints = a.metricsSource.GetNodeMetrics(resourceID, "cpu", period)
|
|
memPoints = a.metricsSource.GetNodeMetrics(resourceID, "memory", period)
|
|
} else {
|
|
cpuPoints = a.metricsSource.GetGuestMetrics(resourceID, "cpu", period)
|
|
memPoints = a.metricsSource.GetGuestMetrics(resourceID, "memory", period)
|
|
}
|
|
|
|
if len(cpuPoints) == 0 && len(memPoints) == 0 {
|
|
return nil
|
|
}
|
|
|
|
summary := &ResourceMetricsSummary{
|
|
ResourceID: resourceID,
|
|
ResourceName: resourceName,
|
|
ResourceType: resourceType,
|
|
Trend: "stable",
|
|
}
|
|
|
|
if len(cpuPoints) > 0 {
|
|
summary.AvgCPU, summary.MaxCPU = computeStats(cpuPoints)
|
|
summary.Trend = computeTrend(cpuPoints)
|
|
}
|
|
if len(memPoints) > 0 {
|
|
summary.AvgMemory, summary.MaxMemory = computeStats(memPoints)
|
|
}
|
|
|
|
return summary
|
|
}
|
|
|
|
func mergeMetricsByTimestamp(allMetrics map[string][]RawMetricPoint) []MetricPoint {
|
|
// Build a map of timestamp -> MetricPoint
|
|
pointsByTime := make(map[int64]*MetricPoint)
|
|
|
|
for metricType, points := range allMetrics {
|
|
for _, p := range points {
|
|
ts := p.Timestamp.Unix()
|
|
if _, exists := pointsByTime[ts]; !exists {
|
|
pointsByTime[ts] = &MetricPoint{Timestamp: p.Timestamp}
|
|
}
|
|
switch metricType {
|
|
case "cpu":
|
|
pointsByTime[ts].CPU = p.Value
|
|
case "memory":
|
|
pointsByTime[ts].Memory = p.Value
|
|
case "disk":
|
|
pointsByTime[ts].Disk = p.Value
|
|
}
|
|
}
|
|
}
|
|
|
|
// Convert to slice and sort by timestamp
|
|
result := make([]MetricPoint, 0, len(pointsByTime))
|
|
for _, p := range pointsByTime {
|
|
result = append(result, *p)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func computeStats(points []RawMetricPoint) (avg, max float64) {
|
|
if len(points) == 0 {
|
|
return 0, 0
|
|
}
|
|
|
|
var sum float64
|
|
for _, p := range points {
|
|
sum += p.Value
|
|
if p.Value > max {
|
|
max = p.Value
|
|
}
|
|
}
|
|
avg = sum / float64(len(points))
|
|
return avg, max
|
|
}
|
|
|
|
func computeTrend(points []RawMetricPoint) string {
|
|
if len(points) < 2 {
|
|
return "stable"
|
|
}
|
|
|
|
// Compare first quarter average to last quarter average
|
|
quarter := len(points) / 4
|
|
if quarter < 1 {
|
|
quarter = 1
|
|
}
|
|
|
|
var firstSum, lastSum float64
|
|
for i := 0; i < quarter; i++ {
|
|
firstSum += points[i].Value
|
|
}
|
|
for i := len(points) - quarter; i < len(points); i++ {
|
|
lastSum += points[i].Value
|
|
}
|
|
|
|
firstAvg := firstSum / float64(quarter)
|
|
lastAvg := lastSum / float64(quarter)
|
|
|
|
diff := lastAvg - firstAvg
|
|
threshold := 5.0 // 5% change threshold
|
|
|
|
if diff > threshold {
|
|
return "growing"
|
|
} else if diff < -threshold {
|
|
return "declining"
|
|
}
|
|
return "stable"
|
|
}
|
|
|
|
// ========== Baseline Provider Adapter ==========
|
|
|
|
// BaselineSource provides access to baseline data
|
|
type BaselineSource interface {
|
|
GetBaseline(resourceID, metric string) (mean, stddev float64, sampleCount int, ok bool)
|
|
GetAllBaselines() map[string]map[string]BaselineData
|
|
}
|
|
|
|
// BaselineData represents baseline data from the source
|
|
type BaselineData struct {
|
|
Mean float64
|
|
StdDev float64
|
|
SampleCount int
|
|
}
|
|
|
|
// BaselineMCPAdapter adapts baseline.Store to MCP BaselineProvider interface
|
|
type BaselineMCPAdapter struct {
|
|
source BaselineSource
|
|
}
|
|
|
|
// NewBaselineMCPAdapter creates a new adapter for baseline data
|
|
func NewBaselineMCPAdapter(source BaselineSource) *BaselineMCPAdapter {
|
|
if source == nil {
|
|
return nil
|
|
}
|
|
return &BaselineMCPAdapter{source: source}
|
|
}
|
|
|
|
// GetBaseline implements mcp.BaselineProvider
|
|
func (a *BaselineMCPAdapter) GetBaseline(resourceID, metric string) *MetricBaseline {
|
|
if a.source == nil {
|
|
return nil
|
|
}
|
|
|
|
mean, stddev, _, ok := a.source.GetBaseline(resourceID, metric)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
|
|
return &MetricBaseline{
|
|
Mean: mean,
|
|
StdDev: stddev,
|
|
Min: mean - 2*stddev, // Approximate
|
|
Max: mean + 2*stddev, // Approximate
|
|
}
|
|
}
|
|
|
|
// GetAllBaselines implements mcp.BaselineProvider
|
|
func (a *BaselineMCPAdapter) GetAllBaselines() map[string]map[string]*MetricBaseline {
|
|
if a.source == nil {
|
|
return nil
|
|
}
|
|
|
|
sourceData := a.source.GetAllBaselines()
|
|
if sourceData == nil {
|
|
return nil
|
|
}
|
|
|
|
result := make(map[string]map[string]*MetricBaseline)
|
|
for resourceID, metrics := range sourceData {
|
|
result[resourceID] = make(map[string]*MetricBaseline)
|
|
for metric, data := range metrics {
|
|
result[resourceID][metric] = &MetricBaseline{
|
|
Mean: data.Mean,
|
|
StdDev: data.StdDev,
|
|
Min: data.Mean - 2*data.StdDev,
|
|
Max: data.Mean + 2*data.StdDev,
|
|
}
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// ========== Pattern Provider Adapter ==========
|
|
|
|
// PatternSource provides access to pattern and prediction data
|
|
type PatternSource interface {
|
|
GetPatterns() []PatternData
|
|
GetPredictions() []PredictionData
|
|
}
|
|
|
|
// PatternData represents pattern data from the source
|
|
type PatternData struct {
|
|
ResourceID string
|
|
PatternType string
|
|
Description string
|
|
Confidence float64
|
|
LastSeen time.Time
|
|
}
|
|
|
|
// PredictionData represents prediction data from the source
|
|
type PredictionData struct {
|
|
ResourceID string
|
|
IssueType string
|
|
PredictedTime time.Time
|
|
Confidence float64
|
|
Recommendation string
|
|
}
|
|
|
|
// PatternMCPAdapter adapts patterns.Detector to MCP PatternProvider interface
|
|
type PatternMCPAdapter struct {
|
|
source PatternSource
|
|
stateGetter StateGetter
|
|
}
|
|
|
|
// NewPatternMCPAdapter creates a new adapter for pattern data
|
|
func NewPatternMCPAdapter(source PatternSource, stateGetter StateGetter) *PatternMCPAdapter {
|
|
if source == nil {
|
|
return nil
|
|
}
|
|
return &PatternMCPAdapter{source: source, stateGetter: stateGetter}
|
|
}
|
|
|
|
// GetPatterns implements mcp.PatternProvider
|
|
func (a *PatternMCPAdapter) GetPatterns() []Pattern {
|
|
if a.source == nil {
|
|
return nil
|
|
}
|
|
|
|
sourcePatterns := a.source.GetPatterns()
|
|
if sourcePatterns == nil {
|
|
return nil
|
|
}
|
|
|
|
result := make([]Pattern, 0, len(sourcePatterns))
|
|
for _, p := range sourcePatterns {
|
|
result = append(result, Pattern{
|
|
ResourceID: p.ResourceID,
|
|
ResourceName: a.getResourceName(p.ResourceID),
|
|
PatternType: p.PatternType,
|
|
Description: p.Description,
|
|
Confidence: p.Confidence,
|
|
LastSeen: p.LastSeen,
|
|
})
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// GetPredictions implements mcp.PatternProvider
|
|
func (a *PatternMCPAdapter) GetPredictions() []Prediction {
|
|
if a.source == nil {
|
|
return nil
|
|
}
|
|
|
|
sourcePredictions := a.source.GetPredictions()
|
|
if sourcePredictions == nil {
|
|
return nil
|
|
}
|
|
|
|
result := make([]Prediction, 0, len(sourcePredictions))
|
|
for _, p := range sourcePredictions {
|
|
result = append(result, Prediction{
|
|
ResourceID: p.ResourceID,
|
|
ResourceName: a.getResourceName(p.ResourceID),
|
|
IssueType: p.IssueType,
|
|
PredictedTime: p.PredictedTime,
|
|
Confidence: p.Confidence,
|
|
Recommendation: p.Recommendation,
|
|
})
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (a *PatternMCPAdapter) getResourceName(resourceID string) string {
|
|
if a.stateGetter == nil {
|
|
return resourceID
|
|
}
|
|
state := a.stateGetter.GetState()
|
|
for _, vm := range state.VMs {
|
|
if fmt.Sprintf("%d", vm.VMID) == resourceID {
|
|
return vm.Name
|
|
}
|
|
}
|
|
for _, ct := range state.Containers {
|
|
if fmt.Sprintf("%d", ct.VMID) == resourceID {
|
|
return ct.Name
|
|
}
|
|
}
|
|
for _, node := range state.Nodes {
|
|
if node.ID == resourceID {
|
|
return node.Name
|
|
}
|
|
}
|
|
return resourceID
|
|
}
|
|
|
|
// ========== Findings Manager Adapter ==========
|
|
|
|
// FindingsManagerSource provides findings management operations
|
|
type FindingsManagerSource interface {
|
|
ResolveFinding(findingID, note string) error
|
|
DismissFinding(findingID, reason, note string) error
|
|
}
|
|
|
|
// FindingsManagerMCPAdapter adapts PatrolService to MCP FindingsManager interface
|
|
type FindingsManagerMCPAdapter struct {
|
|
source FindingsManagerSource
|
|
}
|
|
|
|
// NewFindingsManagerMCPAdapter creates a new adapter for findings management
|
|
func NewFindingsManagerMCPAdapter(source FindingsManagerSource) *FindingsManagerMCPAdapter {
|
|
if source == nil {
|
|
return nil
|
|
}
|
|
return &FindingsManagerMCPAdapter{source: source}
|
|
}
|
|
|
|
// ResolveFinding implements mcp.FindingsManager
|
|
func (a *FindingsManagerMCPAdapter) ResolveFinding(findingID, note string) error {
|
|
if a.source == nil {
|
|
return fmt.Errorf("findings manager not available")
|
|
}
|
|
return a.source.ResolveFinding(findingID, note)
|
|
}
|
|
|
|
// DismissFinding implements mcp.FindingsManager
|
|
func (a *FindingsManagerMCPAdapter) DismissFinding(findingID, reason, note string) error {
|
|
if a.source == nil {
|
|
return fmt.Errorf("findings manager not available")
|
|
}
|
|
return a.source.DismissFinding(findingID, reason, note)
|
|
}
|
|
|
|
// ========== Metadata Updater Adapter ==========
|
|
|
|
// MetadataUpdaterSource provides metadata update operations
|
|
type MetadataUpdaterSource interface {
|
|
SetResourceURL(resourceType, resourceID, url string) error
|
|
}
|
|
|
|
// MetadataUpdaterMCPAdapter adapts ai.Service to MCP MetadataUpdater interface
|
|
type MetadataUpdaterMCPAdapter struct {
|
|
source MetadataUpdaterSource
|
|
}
|
|
|
|
// NewMetadataUpdaterMCPAdapter creates a new adapter for metadata updates
|
|
func NewMetadataUpdaterMCPAdapter(source MetadataUpdaterSource) *MetadataUpdaterMCPAdapter {
|
|
if source == nil {
|
|
return nil
|
|
}
|
|
return &MetadataUpdaterMCPAdapter{source: source}
|
|
}
|
|
|
|
// SetResourceURL implements mcp.MetadataUpdater
|
|
func (a *MetadataUpdaterMCPAdapter) SetResourceURL(resourceType, resourceID, url string) error {
|
|
if a.source == nil {
|
|
return fmt.Errorf("metadata updater not available")
|
|
}
|
|
return a.source.SetResourceURL(resourceType, resourceID, url)
|
|
}
|
|
|
|
// ========== Updates Provider Adapter ==========
|
|
|
|
// UpdatesMonitor is the subset of Monitor methods needed for update operations
|
|
type UpdatesMonitor interface {
|
|
GetState() models.StateSnapshot
|
|
QueueDockerCheckUpdatesCommand(hostID string) (models.DockerHostCommandStatus, error)
|
|
QueueDockerContainerUpdateCommand(hostID, containerID, containerName string) (models.DockerHostCommandStatus, error)
|
|
}
|
|
|
|
// UpdatesConfig provides configuration for update operations
|
|
type UpdatesConfig interface {
|
|
IsDockerUpdateActionsEnabled() bool
|
|
}
|
|
|
|
// UpdatesMCPAdapter adapts Monitor to MCP UpdatesProvider interface
|
|
type UpdatesMCPAdapter struct {
|
|
monitor UpdatesMonitor
|
|
config UpdatesConfig
|
|
}
|
|
|
|
// NewUpdatesMCPAdapter creates a new adapter for update operations
|
|
func NewUpdatesMCPAdapter(monitor UpdatesMonitor, config UpdatesConfig) *UpdatesMCPAdapter {
|
|
if monitor == nil {
|
|
return nil
|
|
}
|
|
return &UpdatesMCPAdapter{monitor: monitor, config: config}
|
|
}
|
|
|
|
// GetPendingUpdates implements mcp.UpdatesProvider
|
|
func (a *UpdatesMCPAdapter) GetPendingUpdates(hostID string) []ContainerUpdateInfo {
|
|
if a.monitor == nil {
|
|
return nil
|
|
}
|
|
|
|
state := a.monitor.GetState()
|
|
var updates []ContainerUpdateInfo
|
|
|
|
for _, host := range state.DockerHosts {
|
|
// Filter by host ID if specified
|
|
if hostID != "" && host.ID != hostID {
|
|
continue
|
|
}
|
|
|
|
for _, container := range host.Containers {
|
|
if container.UpdateStatus == nil {
|
|
continue
|
|
}
|
|
|
|
// Only include containers with updates available or errors
|
|
if !container.UpdateStatus.UpdateAvailable && container.UpdateStatus.Error == "" {
|
|
continue
|
|
}
|
|
|
|
update := ContainerUpdateInfo{
|
|
HostID: host.ID,
|
|
HostName: host.DisplayName,
|
|
ContainerID: container.ID,
|
|
ContainerName: trimContainerName(container.Name),
|
|
Image: container.Image,
|
|
UpdateAvailable: container.UpdateStatus.UpdateAvailable,
|
|
}
|
|
|
|
if container.UpdateStatus.CurrentDigest != "" {
|
|
update.CurrentDigest = container.UpdateStatus.CurrentDigest
|
|
}
|
|
if container.UpdateStatus.LatestDigest != "" {
|
|
update.LatestDigest = container.UpdateStatus.LatestDigest
|
|
}
|
|
if !container.UpdateStatus.LastChecked.IsZero() {
|
|
update.LastChecked = container.UpdateStatus.LastChecked.Unix()
|
|
}
|
|
if container.UpdateStatus.Error != "" {
|
|
update.Error = container.UpdateStatus.Error
|
|
}
|
|
|
|
updates = append(updates, update)
|
|
}
|
|
}
|
|
|
|
return updates
|
|
}
|
|
|
|
// TriggerUpdateCheck implements mcp.UpdatesProvider
|
|
func (a *UpdatesMCPAdapter) TriggerUpdateCheck(hostID string) (DockerCommandStatus, error) {
|
|
if a.monitor == nil {
|
|
return DockerCommandStatus{}, fmt.Errorf("monitor not available")
|
|
}
|
|
|
|
cmdStatus, err := a.monitor.QueueDockerCheckUpdatesCommand(hostID)
|
|
if err != nil {
|
|
return DockerCommandStatus{}, err
|
|
}
|
|
|
|
return DockerCommandStatus{
|
|
ID: cmdStatus.ID,
|
|
Type: cmdStatus.Type,
|
|
Status: cmdStatus.Status,
|
|
Message: cmdStatus.Message,
|
|
}, nil
|
|
}
|
|
|
|
// UpdateContainer implements mcp.UpdatesProvider
|
|
func (a *UpdatesMCPAdapter) UpdateContainer(hostID, containerID, containerName string) (DockerCommandStatus, error) {
|
|
if a.monitor == nil {
|
|
return DockerCommandStatus{}, fmt.Errorf("monitor not available")
|
|
}
|
|
|
|
cmdStatus, err := a.monitor.QueueDockerContainerUpdateCommand(hostID, containerID, containerName)
|
|
if err != nil {
|
|
return DockerCommandStatus{}, err
|
|
}
|
|
|
|
return DockerCommandStatus{
|
|
ID: cmdStatus.ID,
|
|
Type: cmdStatus.Type,
|
|
Status: cmdStatus.Status,
|
|
Message: cmdStatus.Message,
|
|
}, nil
|
|
}
|
|
|
|
// IsUpdateActionsEnabled implements mcp.UpdatesProvider
|
|
func (a *UpdatesMCPAdapter) IsUpdateActionsEnabled() bool {
|
|
if a.config == nil {
|
|
return true // Default to enabled if no config
|
|
}
|
|
return a.config.IsDockerUpdateActionsEnabled()
|
|
}
|
|
|
|
// trimContainerName removes the leading slash from container names
|
|
func trimContainerName(name string) string {
|
|
if len(name) > 0 && name[0] == '/' {
|
|
return name[1:]
|
|
}
|
|
return name
|
|
}
|
|
|
|
// ========== Discovery Provider Adapter ==========
|
|
|
|
// DiscoverySource provides access to AI-powered infrastructure discovery data
|
|
type DiscoverySource interface {
|
|
GetDiscovery(id string) (DiscoverySourceData, error)
|
|
GetDiscoveryByResource(resourceType, hostID, resourceID string) (DiscoverySourceData, error)
|
|
ListDiscoveries() ([]DiscoverySourceData, error)
|
|
ListDiscoveriesByType(resourceType string) ([]DiscoverySourceData, error)
|
|
ListDiscoveriesByHost(hostID string) ([]DiscoverySourceData, error)
|
|
FormatForAIContext(discoveries []DiscoverySourceData) string
|
|
// TriggerDiscovery initiates discovery for a resource, returning discovered data
|
|
TriggerDiscovery(ctx context.Context, resourceType, hostID, resourceID string) (DiscoverySourceData, error)
|
|
}
|
|
|
|
// DiscoverySourceData represents discovery data from the source
|
|
type DiscoverySourceData struct {
|
|
ID string
|
|
ResourceType string
|
|
ResourceID string
|
|
HostID string
|
|
Hostname string
|
|
ServiceType string
|
|
ServiceName string
|
|
ServiceVersion string
|
|
Category string
|
|
CLIAccess string
|
|
Facts []DiscoverySourceFact
|
|
ConfigPaths []string
|
|
DataPaths []string
|
|
LogPaths []string
|
|
Ports []DiscoverySourcePort
|
|
DockerMounts []DiscoverySourceDockerMount // Docker bind mounts (for LXCs/VMs running Docker)
|
|
UserNotes string
|
|
Confidence float64
|
|
AIReasoning string
|
|
DiscoveredAt time.Time
|
|
UpdatedAt time.Time
|
|
}
|
|
|
|
// DiscoverySourceDockerMount represents a Docker bind mount from the source
|
|
type DiscoverySourceDockerMount struct {
|
|
ContainerName string // Docker container name
|
|
Source string // Host path (where to actually write files)
|
|
Destination string // Container path (what the service sees)
|
|
Type string // Mount type: bind, volume, tmpfs
|
|
ReadOnly bool // Whether mount is read-only
|
|
}
|
|
|
|
// DiscoverySourcePort represents a port from the source
|
|
type DiscoverySourcePort struct {
|
|
Port int
|
|
Protocol string
|
|
Process string
|
|
Address string
|
|
}
|
|
|
|
// DiscoverySourceFact represents a fact from the source
|
|
type DiscoverySourceFact struct {
|
|
Category string
|
|
Key string
|
|
Value string
|
|
Source string
|
|
Confidence float64 // 0-1 confidence for this fact
|
|
}
|
|
|
|
// DiscoveryMCPAdapter adapts servicediscovery.Service to MCP DiscoveryProvider interface
|
|
type DiscoveryMCPAdapter struct {
|
|
source DiscoverySource
|
|
}
|
|
|
|
// NewDiscoveryMCPAdapter creates a new adapter for discovery data
|
|
func NewDiscoveryMCPAdapter(source DiscoverySource) *DiscoveryMCPAdapter {
|
|
if source == nil {
|
|
return nil
|
|
}
|
|
return &DiscoveryMCPAdapter{source: source}
|
|
}
|
|
|
|
// GetDiscovery implements tools.DiscoveryProvider
|
|
func (a *DiscoveryMCPAdapter) GetDiscovery(id string) (*ResourceDiscoveryInfo, error) {
|
|
if a.source == nil {
|
|
return nil, fmt.Errorf("discovery source not available")
|
|
}
|
|
|
|
data, err := a.source.GetDiscovery(id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return a.convertToInfo(data), nil
|
|
}
|
|
|
|
// GetDiscoveryByResource implements tools.DiscoveryProvider
|
|
func (a *DiscoveryMCPAdapter) GetDiscoveryByResource(resourceType, hostID, resourceID string) (*ResourceDiscoveryInfo, error) {
|
|
if a.source == nil {
|
|
return nil, fmt.Errorf("discovery source not available")
|
|
}
|
|
|
|
data, err := a.source.GetDiscoveryByResource(resourceType, hostID, resourceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return a.convertToInfo(data), nil
|
|
}
|
|
|
|
// ListDiscoveries implements tools.DiscoveryProvider
|
|
func (a *DiscoveryMCPAdapter) ListDiscoveries() ([]*ResourceDiscoveryInfo, error) {
|
|
if a.source == nil {
|
|
return nil, fmt.Errorf("discovery source not available")
|
|
}
|
|
|
|
dataList, err := a.source.ListDiscoveries()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return a.convertList(dataList), nil
|
|
}
|
|
|
|
// ListDiscoveriesByType implements tools.DiscoveryProvider
|
|
func (a *DiscoveryMCPAdapter) ListDiscoveriesByType(resourceType string) ([]*ResourceDiscoveryInfo, error) {
|
|
if a.source == nil {
|
|
return nil, fmt.Errorf("discovery source not available")
|
|
}
|
|
|
|
dataList, err := a.source.ListDiscoveriesByType(resourceType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return a.convertList(dataList), nil
|
|
}
|
|
|
|
// ListDiscoveriesByHost implements tools.DiscoveryProvider
|
|
func (a *DiscoveryMCPAdapter) ListDiscoveriesByHost(hostID string) ([]*ResourceDiscoveryInfo, error) {
|
|
if a.source == nil {
|
|
return nil, fmt.Errorf("discovery source not available")
|
|
}
|
|
|
|
dataList, err := a.source.ListDiscoveriesByHost(hostID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return a.convertList(dataList), nil
|
|
}
|
|
|
|
// FormatForAIContext implements tools.DiscoveryProvider
|
|
func (a *DiscoveryMCPAdapter) FormatForAIContext(discoveries []*ResourceDiscoveryInfo) string {
|
|
if a.source == nil {
|
|
return ""
|
|
}
|
|
|
|
// Convert back to source data format
|
|
sourceData := make([]DiscoverySourceData, 0, len(discoveries))
|
|
for _, d := range discoveries {
|
|
if d == nil {
|
|
continue
|
|
}
|
|
facts := make([]DiscoverySourceFact, 0, len(d.Facts))
|
|
for _, f := range d.Facts {
|
|
facts = append(facts, DiscoverySourceFact{
|
|
Category: f.Category,
|
|
Key: f.Key,
|
|
Value: f.Value,
|
|
Source: f.Source,
|
|
Confidence: f.Confidence,
|
|
})
|
|
}
|
|
ports := make([]DiscoverySourcePort, 0, len(d.Ports))
|
|
for _, p := range d.Ports {
|
|
ports = append(ports, DiscoverySourcePort{
|
|
Port: p.Port,
|
|
Protocol: p.Protocol,
|
|
Process: p.Process,
|
|
Address: p.Address,
|
|
})
|
|
}
|
|
dockerMounts := make([]DiscoverySourceDockerMount, 0, len(d.BindMounts))
|
|
for _, m := range d.BindMounts {
|
|
dockerMounts = append(dockerMounts, DiscoverySourceDockerMount{
|
|
ContainerName: m.ContainerName,
|
|
Source: m.Source,
|
|
Destination: m.Destination,
|
|
Type: m.Type,
|
|
ReadOnly: m.ReadOnly,
|
|
})
|
|
}
|
|
sourceData = append(sourceData, DiscoverySourceData{
|
|
ID: d.ID,
|
|
ResourceType: d.ResourceType,
|
|
ResourceID: d.ResourceID,
|
|
HostID: d.HostID,
|
|
Hostname: d.Hostname,
|
|
ServiceType: d.ServiceType,
|
|
ServiceName: d.ServiceName,
|
|
ServiceVersion: d.ServiceVersion,
|
|
Category: d.Category,
|
|
CLIAccess: d.CLIAccess,
|
|
Facts: facts,
|
|
ConfigPaths: d.ConfigPaths,
|
|
DataPaths: d.DataPaths,
|
|
Ports: ports,
|
|
DockerMounts: dockerMounts,
|
|
UserNotes: d.UserNotes,
|
|
Confidence: d.Confidence,
|
|
AIReasoning: d.AIReasoning,
|
|
DiscoveredAt: d.DiscoveredAt,
|
|
UpdatedAt: d.UpdatedAt,
|
|
})
|
|
}
|
|
|
|
return a.source.FormatForAIContext(sourceData)
|
|
}
|
|
|
|
// TriggerDiscovery implements tools.DiscoveryProvider
|
|
func (a *DiscoveryMCPAdapter) TriggerDiscovery(ctx context.Context, resourceType, hostID, resourceID string) (*ResourceDiscoveryInfo, error) {
|
|
if a.source == nil {
|
|
return nil, fmt.Errorf("discovery source not available")
|
|
}
|
|
|
|
data, err := a.source.TriggerDiscovery(ctx, resourceType, hostID, resourceID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return a.convertToInfo(data), nil
|
|
}
|
|
|
|
func (a *DiscoveryMCPAdapter) convertToInfo(data DiscoverySourceData) *ResourceDiscoveryInfo {
|
|
if data.ID == "" {
|
|
return nil
|
|
}
|
|
|
|
facts := make([]DiscoveryFact, 0, len(data.Facts))
|
|
for _, f := range data.Facts {
|
|
facts = append(facts, DiscoveryFact{
|
|
Category: f.Category,
|
|
Key: f.Key,
|
|
Value: f.Value,
|
|
Source: f.Source,
|
|
Confidence: f.Confidence,
|
|
})
|
|
}
|
|
|
|
ports := make([]DiscoveryPortInfo, 0, len(data.Ports))
|
|
for _, p := range data.Ports {
|
|
ports = append(ports, DiscoveryPortInfo{
|
|
Port: p.Port,
|
|
Protocol: p.Protocol,
|
|
Process: p.Process,
|
|
Address: p.Address,
|
|
})
|
|
}
|
|
|
|
// Convert DockerMounts to BindMounts
|
|
bindMounts := make([]DiscoveryMount, 0, len(data.DockerMounts))
|
|
for _, m := range data.DockerMounts {
|
|
bindMounts = append(bindMounts, DiscoveryMount{
|
|
ContainerName: m.ContainerName,
|
|
Source: m.Source,
|
|
Destination: m.Destination,
|
|
Type: m.Type,
|
|
ReadOnly: m.ReadOnly,
|
|
})
|
|
}
|
|
|
|
return &ResourceDiscoveryInfo{
|
|
ID: data.ID,
|
|
ResourceType: data.ResourceType,
|
|
ResourceID: data.ResourceID,
|
|
HostID: data.HostID,
|
|
Hostname: data.Hostname,
|
|
ServiceType: data.ServiceType,
|
|
ServiceName: data.ServiceName,
|
|
ServiceVersion: data.ServiceVersion,
|
|
Category: data.Category,
|
|
CLIAccess: data.CLIAccess,
|
|
Facts: facts,
|
|
ConfigPaths: data.ConfigPaths,
|
|
DataPaths: data.DataPaths,
|
|
LogPaths: data.LogPaths,
|
|
Ports: ports,
|
|
BindMounts: bindMounts,
|
|
UserNotes: data.UserNotes,
|
|
Confidence: data.Confidence,
|
|
AIReasoning: data.AIReasoning,
|
|
DiscoveredAt: data.DiscoveredAt,
|
|
UpdatedAt: data.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func (a *DiscoveryMCPAdapter) convertList(dataList []DiscoverySourceData) []*ResourceDiscoveryInfo {
|
|
result := make([]*ResourceDiscoveryInfo, 0, len(dataList))
|
|
for _, data := range dataList {
|
|
if info := a.convertToInfo(data); info != nil {
|
|
result = append(result, info)
|
|
}
|
|
}
|
|
return result
|
|
}
|