1- // Copyright (C) 2026 The Falco Authors
1+ // Copyright (C) 2025 The Falco Authors
22//
33// Licensed under the Apache License, Version 2.0 (the "License");
44// you may not use this file except in compliance with the License.
@@ -18,122 +18,72 @@ package falco
1818
1919import (
2020 "fmt"
21- "strings"
2221
2322 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
23+ "k8s.io/apimachinery/pkg/runtime"
2424 "sigs.k8s.io/structured-merge-diff/v4/typed"
2525
26- "github.com/falcosecurity/falco-operator/internal/pkg/scheme "
26+ "github.com/falcosecurity/falco-operator/internal/pkg/managedfields "
2727)
2828
29- // diff calculates the difference between the current and desired objects.
30- // It accepts either unstructured.Unstructured objects or typed objects that can be converted to unstructured.
29+ const (
30+ // fieldManager is the name used to identify the controller's managed fields.
31+ fieldManager = "falco-controller"
32+ )
33+
34+ // needsUpdate checks if the current object needs to be updated to match the desired state.
35+ // It extracts only the fields managed by this controller and compares them with the desired config.
3136//
32- // This is used to avoid unnecessary API writes on Kubernetes versions < 1.31 where
33- // Server-Side Apply may cause spurious resourceVersion bumps on no-op patches to CRDs .
37+ // This avoids unnecessary API writes on Kubernetes versions < 1.31 where Server-Side Apply
38+ // may cause spurious resourceVersion bumps on no-op patches.
3439// See: https://github.com/kubernetes/kubernetes/issues/124605
35- func diff (current , desired interface {}) (* typed.Comparison , error ) {
36- // Convert inputs to unstructured if needed
37- currentUnstructured , err := toUnstructured (current )
38- if err != nil {
39- return nil , fmt .Errorf ("failed to convert current object to unstructured: %w" , err )
40+ func needsUpdate (current runtime.Object , desired * unstructured.Unstructured ) (bool , error ) {
41+ if current == nil || desired == nil {
42+ return true , nil
4043 }
4144
42- desiredUnstructured , err := toUnstructured (desired )
45+ // Extract only the fields managed by our field manager from the current object
46+ extracted , err := managedfields .ExtractAsUnstructured (current , fieldManager )
4347 if err != nil {
44- return nil , fmt .Errorf ("failed to convert desired object to unstructured : %w" , err )
48+ return true , fmt .Errorf ("failed to extract managed fields : %w" , err )
4549 }
4650
47- // Remove server-managed fields before comparison
48- removeUnwantedFields (currentUnstructured )
49- removeUnwantedFields (desiredUnstructured )
50-
51- // Create a parser to compare the resources
52- parser := scheme .Parser ()
53-
54- currentTypePath := getTypePath (currentUnstructured )
55-
56- // Parse the base resource
57- currentTyped , err := parser .Type (currentTypePath ).FromUnstructured (currentUnstructured .Object )
58- if err != nil {
59- return nil , err
51+ // If no managed fields found, we need to apply
52+ if extracted == nil {
53+ return true , nil
6054 }
6155
62- desiredTypePath := getTypePath (desiredUnstructured )
63- // Parse the user defined resource
64- desiredTyped , err := parser .Type (desiredTypePath ).FromUnstructured (desiredUnstructured .Object )
65- if err != nil {
66- return nil , err
67- }
56+ // Prune empty fields from both objects before comparison
57+ managedfields .PruneEmptyFields (extracted )
58+ managedfields .PruneEmptyFields (desired )
6859
69- return currentTyped .Compare (desiredTyped )
60+ // Compare the extracted managed fields with the desired state
61+ return managedfields .NeedsUpdate (extracted , desired )
7062}
7163
72- // getTypePath returns the schema type path for an unstructured object.
73- func getTypePath (obj * unstructured.Unstructured ) string {
74- apiVersion := obj .GetAPIVersion ()
75- resourceType := obj .GetKind ()
76- gv := strings .Split (apiVersion , "/" )
77-
78- // Build the schema path based on whether it's a core resource or not
79- var typePath string
80- if len (gv ) == 1 {
81- // Core resources like v1 have no group
82- typePath = fmt .Sprintf ("io.k8s.api.core.%s.%s" , gv [0 ], resourceType )
83- } else {
84- // Other resources have group and version
85- typePath = fmt .Sprintf ("io.k8s.api.%s.%s.%s" , apiGroupToSchemaGroup (gv [0 ]), gv [1 ], resourceType )
64+ // diff calculates the difference between the current and desired objects.
65+ // Returns a typed.Comparison that contains Added, Modified, and Removed field sets.
66+ func diff (current runtime.Object , desired * unstructured.Unstructured ) (* typed.Comparison , error ) {
67+ if current == nil || desired == nil {
68+ return nil , fmt .Errorf ("current and desired objects cannot be nil" )
8669 }
8770
88- return typePath
89- }
90-
91- func apiGroupToSchemaGroup (apiGroup string ) string {
92- mappings := map [string ]string {
93- "rbac.authorization.k8s.io" : "rbac" ,
94- "networking.k8s.io" : "networking" ,
95- "certificates.k8s.io" : "certificates" ,
96- "storage.k8s.io" : "storage" ,
97- "admissionregistration.k8s.io" : "admissionregistration" ,
98- "scheduling.k8s.io" : "scheduling" ,
99- "coordination.k8s.io" : "coordination" ,
100- "discovery.k8s.io" : "discovery" ,
71+ // Extract only the fields managed by our field manager
72+ extracted , err := managedfields .ExtractAsUnstructured (current , fieldManager )
73+ if err != nil {
74+ return nil , fmt .Errorf ("failed to extract managed fields: %w" , err )
10175 }
10276
103- if mapped , ok := mappings [ apiGroup ]; ok {
104- return mapped
77+ if extracted == nil {
78+ return nil , fmt . Errorf ( "no managed fields found for field manager %s" , fieldManager )
10579 }
10680
107- return apiGroup
108- }
81+ // Deep copy desired to avoid modifying the original
82+ desiredCopy := desired . DeepCopy ()
10983
110- // removeUnwantedFields removes server-managed fields from the unstructured object
111- // so they don't affect the comparison.
112- func removeUnwantedFields (obj * unstructured.Unstructured ) {
113- unstructured .RemoveNestedField (obj .Object , "metadata" , "uid" )
114- unstructured .RemoveNestedField (obj .Object , "metadata" , "resourceVersion" )
115- unstructured .RemoveNestedField (obj .Object , "metadata" , "managedFields" )
116- unstructured .RemoveNestedField (obj .Object , "status" )
117- unstructured .RemoveNestedField (obj .Object , "metadata" , "creationTimestamp" )
118- unstructured .RemoveNestedField (obj .Object , "spec" , "template" , "metadata" , "creationTimestamp" )
119- unstructured .RemoveNestedField (obj .Object , "spec" , "revisionHistoryLimit" )
120- unstructured .RemoveNestedField (obj .Object , "metadata" , "generateName" )
121- unstructured .RemoveNestedField (obj .Object , "metadata" , "generation" )
122- // Remove the revision field from the annotations.
123- unstructured .RemoveNestedField (obj .Object , "metadata" , "annotations" , "deployment.kubernetes.io/revision" )
124- // Remove the deprecated field from the annotations.
125- unstructured .RemoveNestedField (obj .Object , "metadata" , "annotations" , "deprecated.daemonset.template.generation" )
126- // If the annotations field is empty, remove it.
127- if metadata , ok := obj .Object ["metadata" ].(map [string ]interface {}); ok {
128- if annotations , ok := metadata ["annotations" ].(map [string ]interface {}); ok {
129- if len (annotations ) == 0 {
130- unstructured .RemoveNestedField (obj .Object , "metadata" , "annotations" )
131- }
132- }
133- }
134- // Only for services, remove the clusterIP and clusterIPs fields.
135- if obj .GetKind () == "Service" {
136- unstructured .RemoveNestedField (obj .Object , "spec" , "clusterIP" )
137- unstructured .RemoveNestedField (obj .Object , "spec" , "clusterIPs" )
138- }
84+ // Prune empty fields before comparison
85+ managedfields .PruneEmptyFields (extracted )
86+ managedfields .PruneEmptyFields (desiredCopy )
87+
88+ return managedfields .Compare (extracted , desiredCopy )
13989}
0 commit comments