Files
core/internal/history/diff_timeline.go

266 lines
9.2 KiB
Go

package history
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
)
func diffComponentStates(before, after ComponentStateV1) ([]PatchOp, error) {
var ops []PatchOp
ops = appendPatchStringPtr(ops, "/identity/vendor_serial", strPtr(before.Identity.VendorSerial), strPtr(after.Identity.VendorSerial), true)
ops = appendPatchStringPtr(ops, "/identity/vendor", before.Identity.Vendor, after.Identity.Vendor, false)
ops = appendPatchStringPtr(ops, "/identity/model", before.Identity.Model, after.Identity.Model, false)
ops = appendPatchStringPtr(ops, "/runtime/firmware_version", before.Runtime.FirmwareVersion, after.Runtime.FirmwareVersion, false)
ops = appendPatchStringPtr(ops, "/runtime/health_status", before.Runtime.HealthStatus, after.Runtime.HealthStatus, false)
ops = appendPatchStringPtr(ops, "/runtime/health_status_at", before.Runtime.HealthStatusAt, after.Runtime.HealthStatusAt, false)
ops = appendPatchStringPtr(ops, "/installation/current_machine_id", before.Installation.CurrentMachineID, after.Installation.CurrentMachineID, false)
ops = appendPatchStringPtr(ops, "/installation/installed_at", before.Installation.InstalledAt, after.Installation.InstalledAt, false)
ops = appendPatchStringPtr(ops, "/installation/slot_name", before.Installation.SlotName, after.Installation.SlotName, false)
ops = appendPatchStringPtr(ops, "/metadata/first_seen_at", before.Metadata.FirstSeenAt, after.Metadata.FirstSeenAt, false)
ops = appendPatchStringPtr(ops, "/metadata/component_type", before.Metadata.ComponentType, after.Metadata.ComponentType, false)
return ops, nil
}
func diffAssetStates(before, after AssetStateV1) ([]PatchOp, error) {
var ops []PatchOp
ops = appendPatchStringPtr(ops, "/identity/name", strPtr(before.Identity.Name), strPtr(after.Identity.Name), true)
ops = appendPatchStringPtr(ops, "/identity/vendor_serial", strPtr(before.Identity.VendorSerial), strPtr(after.Identity.VendorSerial), true)
ops = appendPatchStringPtr(ops, "/identity/vendor", before.Identity.Vendor, after.Identity.Vendor, false)
ops = appendPatchStringPtr(ops, "/identity/model", before.Identity.Model, after.Identity.Model, false)
ops = appendPatchStringPtr(ops, "/identity/machine_tag", before.Identity.MachineTag, after.Identity.MachineTag, false)
for key, beforeValue := range before.Firmware.Devices {
afterValue, ok := after.Firmware.Devices[key]
if !ok {
ops = append(ops, PatchOp{Op: "remove", Path: "/firmware/devices/" + escapeJSONPointer(key)})
continue
}
if beforeValue != afterValue {
ops = append(ops, PatchOp{Op: "replace", Path: "/firmware/devices/" + escapeJSONPointer(key), Value: afterValue})
}
}
for key, afterValue := range after.Firmware.Devices {
if _, ok := before.Firmware.Devices[key]; ok {
continue
}
ops = append(ops, PatchOp{Op: "add", Path: "/firmware/devices/" + escapeJSONPointer(key), Value: afterValue})
}
if !equalStringSlices(before.Inventory.InstalledComponentIDs, after.Inventory.InstalledComponentIDs) {
ops = append(ops, PatchOp{Op: "replace", Path: "/inventory/installed_component_ids", Value: after.Inventory.InstalledComponentIDs})
}
return ops, nil
}
func appendPatchStringPtr(ops []PatchOp, path string, before, after *string, required bool) []PatchOp {
b := ""
a := ""
bok := false
aok := false
if before != nil {
b = strings.TrimSpace(*before)
if b != "" || required {
bok = true
}
}
if after != nil {
a = strings.TrimSpace(*after)
if a != "" || required {
aok = true
}
}
switch {
case bok && aok && b == a:
return ops
case !bok && !aok:
return ops
case !aok:
return append(ops, PatchOp{Op: "remove", Path: path})
case !bok:
return append(ops, PatchOp{Op: "add", Path: path, Value: a})
default:
return append(ops, PatchOp{Op: "replace", Path: path, Value: a})
}
}
func strPtr(v string) *string {
s := strings.TrimSpace(v)
return &s
}
func escapeJSONPointer(value string) string {
value = strings.ReplaceAll(value, "~", "~0")
value = strings.ReplaceAll(value, "/", "~1")
return value
}
func (s *Service) insertComponentTimelineFromStates(ctx context.Context, tx *sql.Tx, componentID, changeType, sourceType string, eventTime time.Time, logicalEntityType, logicalEventID string, correlationID *string, before, after *ComponentStateV1) error {
eventType, firmware := componentTimelineProjection(changeType, before, after)
if eventType == "" {
return nil
}
slotForEvent := timelineSlotFromComponentStates(changeType, before, after)
var machineID *string
if after != nil && after.Installation.CurrentMachineID != nil {
machineID = after.Installation.CurrentMachineID
} else if before != nil && before.Installation.CurrentMachineID != nil {
machineID = before.Installation.CurrentMachineID
}
if isPairedComponentTimelineChange(changeType) && machineID != nil {
if err := s.insertTimelineProjectionRawWithSlot(ctx, tx, "asset", *machineID, eventType, sourceType, eventTime, logicalEntityType, logicalEventID, correlationID, machineID, &componentID, firmware, slotForEvent); err != nil {
return err
}
}
return s.insertTimelineProjectionRawWithSlot(ctx, tx, "component", componentID, eventType, sourceType, eventTime, logicalEntityType, logicalEventID, correlationID, machineID, &componentID, firmware, slotForEvent)
}
func (s *Service) insertAssetTimelineFromStates(ctx context.Context, tx *sql.Tx, assetID, changeType, sourceType string, eventTime time.Time, logicalEntityType, logicalEventID string, correlationID *string, before, after *AssetStateV1) error {
eventType, firmware := assetTimelineProjection(changeType, before, after)
if eventType == "" {
return nil
}
return s.insertTimelineProjectionRaw(ctx, tx, "asset", assetID, eventType, sourceType, eventTime, logicalEntityType, logicalEventID, correlationID, &assetID, nil, firmware)
}
func componentTimelineProjection(changeType string, before, after *ComponentStateV1) (string, *string) {
switch strings.TrimSpace(changeType) {
case "COMPONENT_STATUS_SET":
if after == nil || after.Runtime.HealthStatus == nil {
return "", nil
}
switch *after.Runtime.HealthStatus {
case "OK":
return "COMPONENT_OK", nil
case "UNKNOWN":
return "COMPONENT_UNKNOWN", nil
case "WARNING":
return "COMPONENT_WARNING", nil
case "FAILED":
return "COMPONENT_FAILED", nil
default:
return "", nil
}
case "COMPONENT_FIRMWARE_SET":
if after == nil || after.Runtime.FirmwareVersion == nil {
return "", nil
}
current := strings.TrimSpace(*after.Runtime.FirmwareVersion)
if current == "" {
return "", nil
}
if before == nil || before.Runtime.FirmwareVersion == nil || strings.TrimSpace(*before.Runtime.FirmwareVersion) == "" {
v := fmt.Sprintf("- -> %s", current)
return "FIRMWARE_INSTALLED", &v
}
if strings.TrimSpace(*before.Runtime.FirmwareVersion) == current {
return "", nil
}
v := current
return "FIRMWARE_CHANGED", &v
case "COMPONENT_INSTALLED":
return "INSTALLED", nil
case "COMPONENT_REMOVED":
return "REMOVED", nil
case "COMPONENT_REGISTRY_UPDATED":
return "COMPONENT_UPDATED", nil
case "COMPONENT_ROLLBACK_APPLIED":
return "ROLLBACK_APPLIED", nil
case "COMPONENT_FIRST_SEEN_CORRECTED":
return "", nil
default:
return "CHANGE_APPLIED", nil
}
}
func assetTimelineProjection(changeType string, before, after *AssetStateV1) (string, *string) {
switch strings.TrimSpace(changeType) {
case "ASSET_FIRMWARE_DEVICE_SET":
if after == nil {
return "", nil
}
// If exactly one device changed, capture its version for the timeline payload.
changed := 0
var last string
beforeDevices := map[string]string{}
if before != nil {
beforeDevices = before.Firmware.Devices
}
for name, version := range after.Firmware.Devices {
if beforeDevices[name] != version {
changed++
last = version
}
}
for name := range beforeDevices {
if _, ok := after.Firmware.Devices[name]; !ok {
changed++
}
}
if changed == 0 {
return "", nil
}
if strings.TrimSpace(last) != "" {
return "FIRMWARE_CHANGED", &last
}
return "FIRMWARE_CHANGED", nil
case "ASSET_REGISTRY_UPDATED":
return "ASSET_UPDATED", nil
case "ASSET_LOG_COLLECTED":
return "LOG_COLLECTED", nil
case "ASSET_COMPONENT_SET_CHANGED":
return "", nil
case "ASSET_ROLLBACK_APPLIED":
return "ROLLBACK_APPLIED", nil
default:
return "CHANGE_APPLIED", nil
}
}
func isPairedComponentTimelineChange(changeType string) bool {
switch strings.TrimSpace(changeType) {
case "COMPONENT_STATUS_SET", "COMPONENT_FIRMWARE_SET", "COMPONENT_INSTALLED", "COMPONENT_REMOVED":
return true
default:
return false
}
}
func timelineSlotFromComponentStates(changeType string, before, after *ComponentStateV1) *string {
switch strings.TrimSpace(changeType) {
case "COMPONENT_REMOVED":
if before != nil {
return before.Installation.SlotName
}
return nil
case "COMPONENT_INSTALLED":
if after != nil && after.Installation.SlotName != nil {
return after.Installation.SlotName
}
if before != nil {
return before.Installation.SlotName
}
return nil
default:
if after != nil && after.Installation.SlotName != nil {
return after.Installation.SlotName
}
if before != nil {
return before.Installation.SlotName
}
return nil
}
}
func equalStringSlices(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}