Pulse/internal/unifiedresources/activity_changes.go

131 lines
4.2 KiB
Go

package unifiedresources
import (
"crypto/sha1"
"encoding/hex"
"fmt"
"strings"
"time"
)
const (
MetadataActivityType = "activity_type"
MetadataActivityNativeID = "activity_native_id"
MetadataActivityTitle = "activity_title"
MetadataActivityState = "activity_state"
MetadataActivityMessage = "activity_message"
)
// PlatformActivityChange captures read-only provider activity that should be
// preserved on the canonical resource timeline as investigation context.
type PlatformActivityChange struct {
SourceAdapter ChangeSourceAdapter
ActivityType string
NativeID string
Title string
State string
Message string
Actor string
OccurredAt time.Time
RelatedResources []string
Metadata map[string]any
}
// BuildPlatformActivityChange constructs a canonical timeline change for
// provider-emitted read-side activity such as VMware tasks or events.
func BuildPlatformActivityChange(resourceID string, activity PlatformActivityChange) *ResourceChange {
resourceID = CanonicalResourceID(resourceID)
if resourceID == "" {
return nil
}
sourceAdapter := ChangeSourceAdapter(strings.TrimSpace(string(activity.SourceAdapter)))
if sourceAdapter == "" {
return nil
}
activityType := strings.TrimSpace(activity.ActivityType)
nativeID := strings.TrimSpace(activity.NativeID)
title := strings.TrimSpace(activity.Title)
state := strings.TrimSpace(activity.State)
message := strings.TrimSpace(activity.Message)
if activityType == "" || (nativeID == "" && title == "" && message == "") {
return nil
}
observedAt := activity.OccurredAt.UTC()
if observedAt.IsZero() {
observedAt = time.Now().UTC()
}
change := &ResourceChange{
ID: platformActivityChangeID(resourceID, sourceAdapter, activityType, nativeID, observedAt, title, message),
ObservedAt: observedAt,
ResourceID: resourceID,
Kind: ChangeActivity,
SourceType: SourcePlatformEvent,
SourceAdapter: sourceAdapter,
Confidence: ConfidenceHigh,
Actor: strings.TrimSpace(activity.Actor),
Reason: platformActivityReason(activityType, title, state, message),
Metadata: cloneChangeMetadata(activity.Metadata),
}
if !activity.OccurredAt.IsZero() {
occurredAt := activity.OccurredAt.UTC()
change.OccurredAt = &occurredAt
}
if _, exists := change.Metadata[MetadataActivityType]; !exists && activityType != "" {
change.Metadata[MetadataActivityType] = activityType
}
if _, exists := change.Metadata[MetadataActivityNativeID]; !exists && nativeID != "" {
change.Metadata[MetadataActivityNativeID] = nativeID
}
if _, exists := change.Metadata[MetadataActivityTitle]; !exists && title != "" {
change.Metadata[MetadataActivityTitle] = title
}
if _, exists := change.Metadata[MetadataActivityState]; !exists && state != "" {
change.Metadata[MetadataActivityState] = state
}
if _, exists := change.Metadata[MetadataActivityMessage]; !exists && message != "" {
change.Metadata[MetadataActivityMessage] = message
}
change.RelatedResources = uniqueTrimmed(activity.RelatedResources...)
return change
}
func platformActivityChangeID(resourceID string, sourceAdapter ChangeSourceAdapter, activityType, nativeID string, occurredAt time.Time, title, message string) string {
base := nativeID
if strings.TrimSpace(base) == "" {
base = fmt.Sprintf("%s|%s|%s", occurredAt.UTC().Format(time.RFC3339Nano), strings.TrimSpace(title), strings.TrimSpace(message))
}
sum := sha1.Sum([]byte(strings.Join([]string{
CanonicalResourceID(resourceID),
strings.TrimSpace(string(sourceAdapter)),
strings.TrimSpace(activityType),
base,
}, "|")))
return "activity-" + hex.EncodeToString(sum[:])
}
func platformActivityReason(activityType, title, state, message string) string {
activityType = strings.TrimSpace(strings.ReplaceAll(activityType, "_", " "))
title = strings.TrimSpace(title)
state = strings.TrimSpace(state)
message = strings.TrimSpace(message)
if title != "" && state != "" {
return fmt.Sprintf("%s (%s)", title, state)
}
if title != "" {
return title
}
if message != "" {
return message
}
if activityType == "" {
return "Provider activity recorded"
}
return fmt.Sprintf("%s recorded", strings.ToUpper(activityType[:1])+activityType[1:])
}