operatortrace-go/pkg/predicates/ignore_trace_annotation_update.go (148 lines of code) (raw):
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
// pkg/predicates/ignore_trace_annotation_update.go
package predicates
import (
"github.com/Azure/operatortrace/operatortrace-go/pkg/constants"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)
// IgnoreTraceAnnotationUpdatePredicate implements a predicate that ignores updates
// where only the trace ID and span ID annotations, or resource version changes.
type IgnoreTraceAnnotationUpdatePredicate struct {
predicate.Funcs
}
// Update implements the update event check for the predicate.
func (IgnoreTraceAnnotationUpdatePredicate) Update(e event.UpdateEvent) bool {
if e.ObjectOld == nil || e.ObjectNew == nil {
return true
}
oldAnnotations := e.ObjectOld.GetAnnotations()
newAnnotations := e.ObjectNew.GetAnnotations()
// check if metadata except annotations have changed
labelsChanged := !equality.Semantic.DeepEqual(e.ObjectOld.GetLabels(), e.ObjectNew.GetLabels())
finalizersChanged := !equality.Semantic.DeepEqual(e.ObjectOld.GetFinalizers(), e.ObjectNew.GetFinalizers())
ownerReferenceChanged := !equality.Semantic.DeepEqual(e.ObjectOld.GetOwnerReferences(), e.ObjectNew.GetOwnerReferences())
otherAnnotationsChanged := !equalExcept(oldAnnotations, newAnnotations, constants.TraceIDAnnotation, constants.SpanIDAnnotation, constants.TraceIDTimeAnnotation)
// Check if the spec or status fields have changed
specOrStatusChanged := hasSpecOrStatusOrDataChanged(e.ObjectOld, e.ObjectNew)
// if other annotations changed or spec/status changed, we want to process the update
if labelsChanged || finalizersChanged || ownerReferenceChanged || otherAnnotationsChanged || specOrStatusChanged {
return true
}
// Otherwise, indicate the update should not be processed
return false
}
// HasSignificantUpdate returns true if there's a significant difference between two objects,
// ignoring trace/span annotations and resourceVersion changes.
func HasSignificantUpdate(oldObj, newObj runtime.Object) bool {
updateEvent := event.UpdateEvent{
ObjectOld: oldObj.(client.Object),
ObjectNew: newObj.(client.Object),
}
predicate := IgnoreTraceAnnotationUpdatePredicate{}
return predicate.Update(updateEvent)
}
// hasSpecOrStatusOrDataChanged checks if the spec, status, or data fields have changed.
func hasSpecOrStatusOrDataChanged(oldObj, newObj runtime.Object) bool {
oldUnstructured := objToUnstructured(oldObj)
newUnstructured := objToUnstructured(newObj)
// Replace empty structs or slices with nil
replaceEmptyStructsAndSlicesWithNil(oldUnstructured)
replaceEmptyStructsAndSlicesWithNil(newUnstructured)
oldStatus := getFieldExcludingObservedGeneration(oldUnstructured, "status")
newStatus := getFieldExcludingObservedGeneration(newUnstructured, "status")
specChanged := hasFieldChanged(oldUnstructured, newUnstructured, "spec")
statusChanged := !equality.Semantic.DeepEqual(oldStatus, newStatus)
dataChanged := hasFieldChanged(oldUnstructured, newUnstructured, "data")
return specChanged || statusChanged || dataChanged
}
// getFieldExcludingObservedGeneration retrieves the field and excludes the observedGeneration.
func getFieldExcludingObservedGeneration(obj map[string]interface{}, field string) interface{} {
status, found, err := unstructured.NestedFieldNoCopy(obj, field)
if err != nil || !found {
return nil
}
if statusMap, ok := status.(map[string]interface{}); ok {
delete(statusMap, "observedGeneration")
removeTraceAndSpanConditions(statusMap)
return statusMap
}
return status
}
// hasFieldChanged checks if a specific field has changed between old and new unstructured objects.
func hasFieldChanged(oldUnstructured, newUnstructured map[string]interface{}, field string) bool {
oldField, foundOld, errOld := unstructuredNestedFieldNoCopy(oldUnstructured, field)
newField, foundNew, errNew := unstructuredNestedFieldNoCopy(newUnstructured, field)
// If there was an error accessing the field, or if one found and the other not found
if errOld != nil || errNew != nil || foundOld != foundNew {
return true
}
// Check if the fields are semantically equal
return !equality.Semantic.DeepEqual(oldField, newField)
}
// Checks if two maps are equal, ignoring certain keys.
func equalExcept(a, b map[string]string, keysToIgnore ...string) bool {
ignored := make(map[string]struct{})
for _, key := range keysToIgnore {
ignored[key] = struct{}{}
}
for key, aValue := range a {
if _, isIgnored := ignored[key]; !isIgnored {
if bValue, exists := b[key]; !exists || aValue != bValue {
return false
}
}
}
for key := range b {
if _, exists := a[key]; !exists {
if _, isIgnored := ignored[key]; !isIgnored {
return false
}
}
}
return true
}
// Recursively replaces empty structs or slices in the map with nil.
func replaceEmptyStructsAndSlicesWithNil(m map[string]interface{}) {
for k, v := range m {
switch val := v.(type) {
case map[string]interface{}:
if len(val) == 0 {
m[k] = nil
} else {
replaceEmptyStructsAndSlicesWithNil(val)
}
case []interface{}:
if len(val) == 0 {
m[k] = nil
} else {
allElementsEmpty := true
for _, elem := range val {
if elemMap, ok := elem.(map[string]interface{}); ok {
replaceEmptyStructsAndSlicesWithNil(elemMap)
if len(elemMap) > 0 {
allElementsEmpty = false
}
} else {
allElementsEmpty = false
}
}
if allElementsEmpty {
m[k] = nil
}
}
}
}
}
func objToUnstructured(obj runtime.Object) map[string]interface{} {
unstructuredMap, _ := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
return unstructuredMap
}
func unstructuredNestedFieldNoCopy(obj map[string]interface{}, fields ...string) (interface{}, bool, error) {
val, found, err := unstructured.NestedFieldNoCopy(obj, fields...)
if !found || err != nil {
return nil, false, err
}
return val, true, nil
}
// removeTraceAndSpanConditions removes conditions with Type 'TraceID' or 'SpanID' from the status.
func removeTraceAndSpanConditions(statusMap map[string]interface{}) {
conditions, found, err := unstructured.NestedSlice(statusMap, "conditions")
if err != nil || !found {
return
}
filteredConditions := []interface{}{}
for _, condition := range conditions {
if conditionMap, ok := condition.(map[string]interface{}); ok {
conditionType, _, _ := unstructured.NestedString(conditionMap, "type")
if conditionType != "TraceID" && conditionType != "SpanID" {
filteredConditions = append(filteredConditions, condition)
}
}
}
statusMap["conditions"] = filteredConditions
}