Files
pulse/internal/ai/tools/adapters.go
T
2026-07-11 15:19:41 +01:00

1188 lines
34 KiB
Go

package tools
import (
"context"
"fmt"
"strings"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/recovery"
recoverymanager "github.com/rcourtman/pulse-go-rewrite/internal/recovery/manager"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
// AlertManagerToolAdapter adapts alerts.Manager to AlertProvider interface
type AlertManagerToolAdapter struct {
manager AlertManager
}
// AlertManager interface matches what alerts.Manager provides
type AlertManager interface {
GetActiveAlerts() []alerts.Alert
GetRecentlyResolved() []models.ResolvedAlert
}
// NewAlertManagerToolAdapter creates a new adapter for alert manager
func NewAlertManagerToolAdapter(manager AlertManager) *AlertManagerToolAdapter {
if manager == nil {
return nil
}
return &AlertManagerToolAdapter{manager: manager}
}
// GetActiveAlerts implements AlertProvider
func (a *AlertManagerToolAdapter) 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
}
// GetRecentlyResolved implements AlertProvider
func (a *AlertManagerToolAdapter) GetRecentlyResolved(minutes int) []models.ResolvedAlert {
if a.manager == nil {
return nil
}
return a.manager.GetRecentlyResolved()
}
// GuestConfigSource provides guest configuration data with context.
type GuestConfigSource interface {
GetGuestConfig(ctx context.Context, guestType, instance, node string, vmID int) (map[string]interface{}, error)
}
// GuestConfigToolAdapter adapts monitoring config access to GuestConfigProvider interface.
type GuestConfigToolAdapter struct {
source GuestConfigSource
timeout time.Duration
}
// NewGuestConfigToolAdapter creates a new adapter for guest config data.
func NewGuestConfigToolAdapter(source GuestConfigSource) *GuestConfigToolAdapter {
if source == nil {
return nil
}
return &GuestConfigToolAdapter{
source: source,
timeout: 5 * time.Second,
}
}
// GetGuestConfig implements GuestConfigProvider.
func (a *GuestConfigToolAdapter) 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)
}
// BackupToolAdapter adapts backup data sources to BackupProvider interface.
// Uses functional getters so callers can wire ReadState or StateSnapshot sources.
type BackupToolAdapter struct {
getBackups func() models.Backups
getPBSInstances func() []models.PBSInstance
}
// NewBackupToolAdapter creates a new adapter for backup data.
// Both getters must be non-nil; returns nil if either is nil.
func NewBackupToolAdapter(getBackups func() models.Backups, getPBSInstances func() []models.PBSInstance) *BackupToolAdapter {
if getBackups == nil || getPBSInstances == nil {
return nil
}
return &BackupToolAdapter{getBackups: getBackups, getPBSInstances: getPBSInstances}
}
// GetBackups implements BackupProvider
func (a *BackupToolAdapter) GetBackups() models.Backups {
if a.getBackups == nil {
return models.Backups{}
}
return a.getBackups()
}
// GetPBSInstances implements BackupProvider
func (a *BackupToolAdapter) GetPBSInstances() []models.PBSInstance {
if a.getPBSInstances == nil {
return nil
}
return a.getPBSInstances()
}
// ReplicationToolAdapter adapts replication data sources to ReplicationProvider.
// Uses a functional getter so callers can wire ReadState or StateSnapshot sources.
type ReplicationToolAdapter struct {
getJobs func() []models.ReplicationJob
}
// NewReplicationToolAdapter creates a new adapter for replication data.
func NewReplicationToolAdapter(getJobs func() []models.ReplicationJob) *ReplicationToolAdapter {
if getJobs == nil {
return nil
}
return &ReplicationToolAdapter{getJobs: getJobs}
}
// GetReplicationJobs implements ReplicationProvider.
func (a *ReplicationToolAdapter) GetReplicationJobs() []models.ReplicationJob {
if a.getJobs == nil {
return nil
}
return a.getJobs()
}
// ConnectionHealthToolAdapter adapts connection health data sources to ConnectionHealthProvider.
// Uses a functional getter so callers can wire ReadState or StateSnapshot sources.
type ConnectionHealthToolAdapter struct {
getHealth func() map[string]bool
}
// NewConnectionHealthToolAdapter creates a new adapter for connection health data.
func NewConnectionHealthToolAdapter(getHealth func() map[string]bool) *ConnectionHealthToolAdapter {
if getHealth == nil {
return nil
}
return &ConnectionHealthToolAdapter{getHealth: getHealth}
}
// GetConnectionHealth implements ConnectionHealthProvider.
func (a *ConnectionHealthToolAdapter) GetConnectionHealth() map[string]bool {
if a.getHealth == nil {
return nil
}
return a.getHealth()
}
// RecoveryPointsToolAdapter provides Assistant tools access to persisted recovery points.
// It is org-scoped to avoid leaking cross-tenant data.
type RecoveryPointsToolAdapter struct {
manager *recoverymanager.Manager
orgID string
timeout time.Duration
}
func NewRecoveryPointsToolAdapter(manager *recoverymanager.Manager, orgID string) *RecoveryPointsToolAdapter {
if manager == nil {
return nil
}
if orgID == "" {
orgID = "default"
}
return &RecoveryPointsToolAdapter{
manager: manager,
orgID: orgID,
timeout: 5 * time.Second,
}
}
func (a *RecoveryPointsToolAdapter) ListPoints(ctx context.Context, opts recovery.ListPointsOptions) ([]recovery.RecoveryPoint, int, error) {
if a == nil || a.manager == nil {
return nil, 0, fmt.Errorf("recovery points provider not available")
}
ctx, cancel := context.WithTimeout(ctx, a.timeout)
defer cancel()
store, err := a.manager.StoreForOrg(a.orgID)
if err != nil {
return nil, 0, err
}
return store.ListPoints(ctx, opts)
}
// DiskHealthToolAdapter adapts unified read-state hosts to DiskHealthProvider.
type DiskHealthToolAdapter struct {
readState unifiedresources.ReadState
}
// NewDiskHealthToolAdapter creates a new adapter for disk health data
func NewDiskHealthToolAdapter(readState unifiedresources.ReadState) *DiskHealthToolAdapter {
if readState == nil {
return nil
}
return &DiskHealthToolAdapter{readState: readState}
}
// GetHosts implements DiskHealthProvider
func (a *DiskHealthToolAdapter) GetHosts() []*unifiedresources.HostView {
if a.readState == nil {
return nil
}
return a.readState.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
}
// MetricsHistoryToolAdapter adapts the metrics history to MetricsHistoryProvider interface
type MetricsHistoryToolAdapter struct {
readState unifiedresources.ReadState
metricsSource MetricsSource
}
// NewMetricsHistoryToolAdapter creates a new adapter for metrics history.
// readState is required for iterating VMs/Containers/Nodes in GetAllMetricsSummary.
func NewMetricsHistoryToolAdapter(metricsSource MetricsSource, readState unifiedresources.ReadState) *MetricsHistoryToolAdapter {
if metricsSource == nil || readState == nil {
return nil
}
return &MetricsHistoryToolAdapter{
readState: readState,
metricsSource: metricsSource,
}
}
// GetResourceMetrics implements MetricsHistoryProvider
func (a *MetricsHistoryToolAdapter) 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 MetricsHistoryProvider.
// Uses ReadState to iterate VMs, Containers, and Nodes.
func (a *MetricsHistoryToolAdapter) GetAllMetricsSummary(period time.Duration) (map[string]ResourceMetricsSummary, error) {
if a.metricsSource == nil || a.readState == nil {
return nil, nil
}
result := make(map[string]ResourceMetricsSummary)
for _, vm := range a.readState.VMs() {
vmID := fmt.Sprintf("%d", vm.VMID())
if summary := a.computeSummary(vmID, vm.Name(), "vm", period); summary != nil {
result[vmID] = *summary
}
}
for _, ct := range a.readState.Containers() {
ctID := fmt.Sprintf("%d", ct.VMID())
if summary := a.computeSummary(ctID, ct.Name(), "system-container", period); summary != nil {
result[ctID] = *summary
}
}
// Nodes: SourceID() returns the legacy node ID that MetricsSource indexes by.
for _, node := range a.readState.Nodes() {
nodeID := node.SourceID()
if nodeID == "" {
continue
}
if summary := a.computeSummary(nodeID, node.Name(), "node", period); summary != nil {
result[nodeID] = *summary
}
}
return result, nil
}
func (a *MetricsHistoryToolAdapter) 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
}
// BaselineToolAdapter adapts baseline.Store to BaselineProvider interface
type BaselineToolAdapter struct {
source BaselineSource
}
// NewBaselineToolAdapter creates a new adapter for baseline data
func NewBaselineToolAdapter(source BaselineSource) *BaselineToolAdapter {
if source == nil {
return nil
}
return &BaselineToolAdapter{source: source}
}
// GetBaseline implements BaselineProvider
func (a *BaselineToolAdapter) 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 BaselineProvider
func (a *BaselineToolAdapter) 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
}
// PatternToolAdapter adapts patterns.Detector to PatternProvider interface
type PatternToolAdapter struct {
source PatternSource
readState unifiedresources.ReadState
}
// NewPatternToolAdapter creates a new adapter for pattern data.
// readState is optional: when provided, resource name lookups resolve via
// ReadState views; when nil, raw resource IDs are returned as names.
func NewPatternToolAdapter(source PatternSource, readState unifiedresources.ReadState) *PatternToolAdapter {
if source == nil {
return nil
}
return &PatternToolAdapter{source: source, readState: readState}
}
// GetPatterns implements PatternProvider
func (a *PatternToolAdapter) 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 PatternProvider
func (a *PatternToolAdapter) 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 *PatternToolAdapter) getResourceName(resourceID string) string {
if a.readState == nil {
return resourceID
}
for _, vm := range a.readState.VMs() {
if fmt.Sprintf("%d", vm.VMID()) == resourceID {
return vm.Name()
}
}
for _, ct := range a.readState.Containers() {
if fmt.Sprintf("%d", ct.VMID()) == resourceID {
return ct.Name()
}
}
// Nodes: SourceID() returns the legacy node ID that patterns store.
for _, node := range a.readState.Nodes() {
if node.SourceID() == 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
}
// FindingsManagerToolAdapter adapts PatrolService to the Assistant findings manager provider.
type FindingsManagerToolAdapter struct {
source FindingsManagerSource
}
// NewFindingsManagerToolAdapter creates a new adapter for findings management
func NewFindingsManagerToolAdapter(source FindingsManagerSource) *FindingsManagerToolAdapter {
if source == nil {
return nil
}
return &FindingsManagerToolAdapter{source: source}
}
// ResolveFinding implements FindingsManager
func (a *FindingsManagerToolAdapter) ResolveFinding(findingID, note string) error {
if a.source == nil {
return fmt.Errorf("findings manager not available")
}
return a.source.ResolveFinding(findingID, note)
}
// DismissFinding implements FindingsManager
func (a *FindingsManagerToolAdapter) 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
}
// MetadataUpdaterToolAdapter adapts ai.Service to the Assistant metadata updater provider.
type MetadataUpdaterToolAdapter struct {
source MetadataUpdaterSource
}
// NewMetadataUpdaterToolAdapter creates a new adapter for metadata updates
func NewMetadataUpdaterToolAdapter(source MetadataUpdaterSource) *MetadataUpdaterToolAdapter {
if source == nil {
return nil
}
return &MetadataUpdaterToolAdapter{source: source}
}
// SetResourceURL implements MetadataUpdater
func (a *MetadataUpdaterToolAdapter) 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 ==========
// UpdatesCommandRunner is the subset of Monitor methods needed for Docker update commands.
// It does not include GetState — Docker host iteration uses a functional getter instead.
type UpdatesCommandRunner interface {
QueueDockerCheckUpdatesCommand(hostID string) (models.DockerHostCommandStatus, error)
QueueDockerContainerUpdateCommand(hostID, containerID, containerName string) (models.DockerHostCommandStatus, error)
GetDockerCommandStatus(commandID string) (models.DockerHostCommandStatus, bool)
}
// UpdatesConfig provides configuration for update operations
type UpdatesConfig interface {
IsDockerUpdateActionsEnabled() bool
}
// UpdatesToolAdapter adapts Monitor to UpdatesProvider interface.
// Uses a functional getter for Docker host iteration (decoupled from GetState)
// and a command runner for Docker update operations.
type UpdatesToolAdapter struct {
readState unifiedresources.ReadState
commands UpdatesCommandRunner
config UpdatesConfig
}
// NewUpdatesToolAdapter creates a new adapter for update operations.
// readState provides Docker host/container views; commands provides update command execution.
func NewUpdatesToolAdapter(readState unifiedresources.ReadState, commands UpdatesCommandRunner, config UpdatesConfig) *UpdatesToolAdapter {
if readState == nil || commands == nil {
return nil
}
return &UpdatesToolAdapter{readState: readState, commands: commands, config: config}
}
// GetPendingUpdates implements UpdatesProvider
func (a *UpdatesToolAdapter) GetPendingUpdates(hostID string) []ContainerUpdateInfo {
if a.readState == nil {
return nil
}
hostLabels := make(map[string]string)
for _, host := range a.readState.DockerHosts() {
if host == nil {
continue
}
label := strings.TrimSpace(host.Name())
if label == "" {
label = strings.TrimSpace(host.Hostname())
}
if label == "" {
label = strings.TrimSpace(host.ID())
}
if resourceID := strings.TrimSpace(host.ID()); resourceID != "" {
hostLabels[resourceID] = label
}
if sourceID := strings.TrimSpace(host.HostSourceID()); sourceID != "" {
hostLabels[sourceID] = label
}
}
var updates []ContainerUpdateInfo
for _, container := range a.readState.DockerContainers() {
if container == nil {
continue
}
targetID := strings.TrimSpace(container.HostSourceID())
if targetID == "" {
targetID = strings.TrimSpace(container.ParentID())
}
if hostID != "" && targetID != hostID && strings.TrimSpace(container.ParentID()) != hostID {
continue
}
updateStatus := container.UpdateStatus()
if updateStatus == nil {
continue
}
// Only include containers with updates available or errors.
if !updateStatus.UpdateAvailable && updateStatus.Error == "" {
continue
}
containerID := strings.TrimSpace(container.ContainerID())
if containerID == "" {
containerID = strings.TrimSpace(container.ID())
}
update := ContainerUpdateInfo{
TargetID: targetID,
HostName: hostLabels[targetID],
ContainerID: containerID,
ContainerName: trimContainerName(container.Name()),
Image: container.Image(),
UpdateAvailable: updateStatus.UpdateAvailable,
}
if updateStatus.CurrentDigest != "" {
update.CurrentDigest = updateStatus.CurrentDigest
}
if updateStatus.LatestDigest != "" {
update.LatestDigest = updateStatus.LatestDigest
}
if !updateStatus.LastChecked.IsZero() {
update.LastChecked = updateStatus.LastChecked.Unix()
}
if updateStatus.Error != "" {
update.Error = updateStatus.Error
}
if update.HostName == "" {
update.HostName = targetID
}
updates = append(updates, update)
}
return updates
}
// TriggerUpdateCheck implements UpdatesProvider
func (a *UpdatesToolAdapter) TriggerUpdateCheck(hostID string) (DockerCommandStatus, error) {
if a.commands == nil {
return DockerCommandStatus{}, fmt.Errorf("monitor not available")
}
cmdStatus, err := a.commands.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 UpdatesProvider
func (a *UpdatesToolAdapter) UpdateContainer(hostID, containerID, containerName string) (DockerCommandStatus, error) {
return DockerCommandStatus{}, fmt.Errorf("Docker container updates are retired until canonical lifecycle delivery and compensation are available")
}
// GetCommandStatus implements UpdatesProvider
func (a *UpdatesToolAdapter) GetCommandStatus(commandID string) (DockerCommandStatus, bool) {
if a.commands == nil {
return DockerCommandStatus{}, false
}
cmdStatus, ok := a.commands.GetDockerCommandStatus(commandID)
if !ok {
return DockerCommandStatus{}, false
}
return DockerCommandStatus{
ID: cmdStatus.ID,
Type: cmdStatus.Type,
Status: cmdStatus.Status,
Message: cmdStatus.Message,
FailureReason: cmdStatus.FailureReason,
}, true
}
// IsUpdateActionsEnabled implements UpdatesProvider
func (a *UpdatesToolAdapter) 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, targetID, resourceID string) (DiscoverySourceData, error)
ListDiscoveries() ([]DiscoverySourceData, error)
ListDiscoveriesByType(resourceType string) ([]DiscoverySourceData, error)
ListDiscoveriesByTarget(targetID string) ([]DiscoverySourceData, error)
FormatForAIContext(discoveries []DiscoverySourceData) string
// TriggerDiscovery initiates discovery for a resource, returning discovered data
TriggerDiscovery(ctx context.Context, resourceType, targetID, resourceID string, force bool) (DiscoverySourceData, error)
}
// DiscoverySourceData represents discovery data from the source
type DiscoverySourceData struct {
ID string
ResourceType string
ResourceID string
TargetID string
AgentID 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)
SuggestedURL string
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
}
// DiscoveryToolAdapter adapts servicediscovery.Service to DiscoveryProvider interface
type DiscoveryToolAdapter struct {
source DiscoverySource
}
// NewDiscoveryToolAdapter creates a new adapter for discovery data
func NewDiscoveryToolAdapter(source DiscoverySource) *DiscoveryToolAdapter {
if source == nil {
return nil
}
return &DiscoveryToolAdapter{source: source}
}
// GetDiscovery implements tools.DiscoveryProvider
func (a *DiscoveryToolAdapter) 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 *DiscoveryToolAdapter) GetDiscoveryByResource(resourceType, targetID, resourceID string) (*ResourceDiscoveryInfo, error) {
if a.source == nil {
return nil, fmt.Errorf("discovery source not available")
}
data, err := a.source.GetDiscoveryByResource(resourceType, targetID, resourceID)
if err != nil {
return nil, err
}
return a.convertToInfo(data), nil
}
// ListDiscoveries implements tools.DiscoveryProvider
func (a *DiscoveryToolAdapter) 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 *DiscoveryToolAdapter) 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
}
// ListDiscoveriesByTarget implements tools.DiscoveryProvider
func (a *DiscoveryToolAdapter) ListDiscoveriesByTarget(targetID string) ([]*ResourceDiscoveryInfo, error) {
if a.source == nil {
return nil, fmt.Errorf("discovery source not available")
}
dataList, err := a.source.ListDiscoveriesByTarget(targetID)
if err != nil {
return nil, err
}
return a.convertList(dataList), nil
}
// FormatForAIContext implements tools.DiscoveryProvider
func (a *DiscoveryToolAdapter) 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,
})
}
targetID := strings.TrimSpace(d.TargetID)
sourceData = append(sourceData, DiscoverySourceData{
ID: d.ID,
ResourceType: d.ResourceType,
ResourceID: d.ResourceID,
TargetID: targetID,
AgentID: d.AgentID,
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 *DiscoveryToolAdapter) TriggerDiscovery(ctx context.Context, resourceType, targetID, resourceID string, force bool) (*ResourceDiscoveryInfo, error) {
if a.source == nil {
return nil, fmt.Errorf("discovery source not available")
}
data, err := a.source.TriggerDiscovery(ctx, resourceType, targetID, resourceID, force)
if err != nil {
return nil, err
}
return a.convertToInfo(data), nil
}
func (a *DiscoveryToolAdapter) 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,
})
}
targetID := strings.TrimSpace(data.TargetID)
agentID := strings.TrimSpace(data.AgentID)
if agentID == "" && data.ResourceType == "agent" {
agentID = targetID
}
return &ResourceDiscoveryInfo{
ID: data.ID,
ResourceType: data.ResourceType,
ResourceID: data.ResourceID,
TargetID: targetID,
AgentID: agentID,
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 *DiscoveryToolAdapter) 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
}