|
| 1 | +package service |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "encoding/json" |
| 6 | + "fmt" |
| 7 | + "math" |
| 8 | + "reflect" |
| 9 | + |
| 10 | + jsonpatch "github.com/evanphx/json-patch/v5" |
| 11 | + "github.com/go-openapi/jsonpointer" |
| 12 | + apierrors "k8s.io/apimachinery/pkg/api/errors" |
| 13 | + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" |
| 14 | + "k8s.io/apimachinery/pkg/types" |
| 15 | + "sigs.k8s.io/controller-runtime/pkg/client" |
| 16 | +) |
| 17 | + |
| 18 | +func IgnoreJSONPaths(obj unstructured.Unstructured, ignorePaths []string) (unstructured.Unstructured, error) { |
| 19 | + ops := make([]map[string]string, 0) |
| 20 | + for _, path := range ignorePaths { |
| 21 | + ops = append(ops, map[string]string{ |
| 22 | + "op": "remove", |
| 23 | + "path": path, |
| 24 | + }) |
| 25 | + } |
| 26 | + |
| 27 | + patchJSON, err := json.Marshal(ops) |
| 28 | + if err != nil { |
| 29 | + return obj, fmt.Errorf("failed to marshal json patch representation: %w", err) |
| 30 | + } |
| 31 | + |
| 32 | + raw, err := json.Marshal(obj.Object) |
| 33 | + if err != nil { |
| 34 | + return obj, fmt.Errorf("failed to marshal base kubernetes object for patch: %w", err) |
| 35 | + } |
| 36 | + |
| 37 | + patch, err := jsonpatch.DecodePatch(patchJSON) |
| 38 | + if err != nil { |
| 39 | + return obj, fmt.Errorf("failed to decode json patch object: %w", err) |
| 40 | + } |
| 41 | + |
| 42 | + modified, err := patch.Apply(raw) |
| 43 | + if err != nil { |
| 44 | + return obj, fmt.Errorf("failed to apply patch: %w", err) |
| 45 | + } |
| 46 | + |
| 47 | + var result map[string]interface{} |
| 48 | + if err := json.Unmarshal(modified, &result); err != nil { |
| 49 | + return obj, fmt.Errorf("failed to unmarshal patched object: %w", err) |
| 50 | + } |
| 51 | + |
| 52 | + obj.Object = result |
| 53 | + return obj, nil |
| 54 | +} |
| 55 | + |
| 56 | +func BackFillJSONPaths(ctx context.Context, c client.Client, obj unstructured.Unstructured, ignorePaths []string) (unstructured.Unstructured, error) { |
| 57 | + current := &unstructured.Unstructured{} |
| 58 | + current.SetGroupVersionKind(obj.GroupVersionKind()) |
| 59 | + |
| 60 | + key := types.NamespacedName{ |
| 61 | + Name: obj.GetName(), |
| 62 | + Namespace: obj.GetNamespace(), |
| 63 | + } |
| 64 | + |
| 65 | + if err := c.Get(ctx, key, current); err != nil { |
| 66 | + if apierrors.IsNotFound(err) { |
| 67 | + return obj, nil // Object doesn't exist, skip |
| 68 | + } |
| 69 | + return unstructured.Unstructured{}, fmt.Errorf("failed to get live object: %w", err) |
| 70 | + } |
| 71 | + |
| 72 | + // Compare and overwrite ignored paths |
| 73 | + for _, path := range ignorePaths { |
| 74 | + ptr, err := jsonpointer.New(path) |
| 75 | + if err != nil { |
| 76 | + return unstructured.Unstructured{}, fmt.Errorf("invalid JSON pointer %q: %w", path, err) |
| 77 | + } |
| 78 | + |
| 79 | + liveVal, _, err := ptr.Get(current.Object) |
| 80 | + if err != nil { |
| 81 | + continue // Ignore missing path |
| 82 | + } |
| 83 | + |
| 84 | + desiredVal, _, err := ptr.Get(obj.Object) |
| 85 | + if err != nil || !flexEqual(liveVal, desiredVal) { |
| 86 | + // Set the desired object's field to the live value |
| 87 | + _, err = ptr.Set(obj.Object, liveVal) |
| 88 | + if err != nil { |
| 89 | + return unstructured.Unstructured{}, fmt.Errorf("failed to set path %s: %w", path, err) |
| 90 | + } |
| 91 | + } |
| 92 | + } |
| 93 | + |
| 94 | + return obj, nil |
| 95 | +} |
| 96 | + |
| 97 | +type normalizerKey struct { |
| 98 | + Kind string |
| 99 | + Name string |
| 100 | + Namespace string |
| 101 | + BackFill bool |
| 102 | +} |
| 103 | + |
| 104 | +func matchesKey(obj unstructured.Unstructured, key normalizerKey) (bool, bool) { |
| 105 | + if key.Kind != "" && obj.GetKind() != key.Kind { |
| 106 | + return false, key.BackFill |
| 107 | + } |
| 108 | + if key.Name != "" && obj.GetName() != key.Name { |
| 109 | + return false, key.BackFill |
| 110 | + } |
| 111 | + if key.Namespace != "" && obj.GetNamespace() != key.Namespace { |
| 112 | + return false, key.BackFill |
| 113 | + } |
| 114 | + return true, key.BackFill |
| 115 | +} |
| 116 | + |
| 117 | +// flexEqual compares two values with special handling for numeric types. |
| 118 | +// This is necessary because JSON unmarshalling (used in Unstructured objects) |
| 119 | +// treats all numbers as float64 by default, while Kubernetes API types (like int32/int64) |
| 120 | +// retain their original integer type when fetched using controller-runtime's client. |
| 121 | +// |
| 122 | +// For example: |
| 123 | +// - A value like `replicas: 1` from YAML becomes float64(1.0) |
| 124 | +// - The same field from the Kubernetes API may be int64(1) or int32(1) |
| 125 | +// - reflect.DeepEqual would return false even though they represent the same number |
| 126 | +// |
| 127 | +// This function ensures such semantically equal numeric values are treated as equal. |
| 128 | +func flexEqual(a, b interface{}) bool { |
| 129 | + // Compare numeric types with type coercion |
| 130 | + if isNumber(a) && isNumber(b) { |
| 131 | + af := toFloat64(a) |
| 132 | + bf := toFloat64(b) |
| 133 | + return math.Abs(af-bf) < 1e-9 // tolerate float precision errors |
| 134 | + } |
| 135 | + |
| 136 | + // Fall back to deep equal |
| 137 | + return reflect.DeepEqual(a, b) |
| 138 | +} |
| 139 | + |
| 140 | +func isNumber(v interface{}) bool { |
| 141 | + switch v.(type) { |
| 142 | + case int, int8, int16, int32, int64, |
| 143 | + uint, uint8, uint16, uint32, uint64, |
| 144 | + float32, float64: |
| 145 | + return true |
| 146 | + default: |
| 147 | + return false |
| 148 | + } |
| 149 | +} |
| 150 | + |
| 151 | +func toFloat64(v interface{}) float64 { |
| 152 | + switch n := v.(type) { |
| 153 | + case int: |
| 154 | + return float64(n) |
| 155 | + case int8: |
| 156 | + return float64(n) |
| 157 | + case int16: |
| 158 | + return float64(n) |
| 159 | + case int32: |
| 160 | + return float64(n) |
| 161 | + case int64: |
| 162 | + return float64(n) |
| 163 | + case uint: |
| 164 | + return float64(n) |
| 165 | + case uint8: |
| 166 | + return float64(n) |
| 167 | + case uint16: |
| 168 | + return float64(n) |
| 169 | + case uint32: |
| 170 | + return float64(n) |
| 171 | + case uint64: |
| 172 | + return float64(n) |
| 173 | + case float32: |
| 174 | + return float64(n) |
| 175 | + case float64: |
| 176 | + return n |
| 177 | + default: |
| 178 | + return math.NaN() |
| 179 | + } |
| 180 | +} |
0 commit comments