@@ -34,6 +34,7 @@ import (
3434 "sigs.k8s.io/controller-runtime/pkg/client"
3535
3636 ssaerrors "github.com/fluxcd/pkg/ssa/errors"
37+ "github.com/fluxcd/pkg/ssa/jsondiff"
3738 "github.com/fluxcd/pkg/ssa/utils"
3839)
3940
@@ -68,6 +69,12 @@ type ApplyOptions struct {
6869 // CustomStageKinds defines a set of Kubernetes resource types that should be applied
6970 // in a separate stage after CRDs and before namespaced objects.
7071 CustomStageKinds map [schema.GroupKind ]struct {} `json:"customStageKinds,omitempty"`
72+
73+ // DriftIgnoreRules defines a list of JSON pointer ignore rules that are used to
74+ // remove specific fields from objects before applying them.
75+ // This is useful for ignoring fields that are managed by other controllers
76+ // (e.g. VPA, HPA) and would otherwise cause drift.
77+ DriftIgnoreRules []jsondiff.IgnoreRule `json:"driftIgnoreRules,omitempty"`
7178}
7279
7380// ApplyCleanupOptions defines which metadata entries are to be removed before applying objects.
@@ -133,6 +140,13 @@ func (m *ResourceManager) Apply(ctx context.Context, object *unstructured.Unstru
133140 }
134141
135142 appliedObject := object .DeepCopy ()
143+
144+ if existingObject .GetResourceVersion () != "" && len (opts .DriftIgnoreRules ) > 0 {
145+ if err := removeIgnoredFields (appliedObject , opts .DriftIgnoreRules ); err != nil {
146+ return nil , err
147+ }
148+ }
149+
136150 if err := m .apply (ctx , appliedObject ); err != nil {
137151 return nil , fmt .Errorf ("%s apply failed: %w" , utils .FmtUnstructured (appliedObject ), err )
138152 }
@@ -232,9 +246,14 @@ func (m *ResourceManager) ApplyAll(ctx context.Context, objects []*unstructured.
232246 }
233247 }
234248
235- for _ , object := range toApply {
249+ for i , object := range toApply {
236250 if object != nil {
237251 appliedObject := object .DeepCopy ()
252+ if changes [i ].Action != CreatedAction && len (opts .DriftIgnoreRules ) > 0 {
253+ if err := removeIgnoredFields (appliedObject , opts .DriftIgnoreRules ); err != nil {
254+ return nil , err
255+ }
256+ }
238257 if err := m .apply (ctx , appliedObject ); err != nil {
239258 return nil , fmt .Errorf ("%s apply failed: %w" , utils .FmtUnstructured (appliedObject ), err )
240259 }
@@ -424,3 +443,33 @@ func (m *ResourceManager) shouldSkipApply(desiredObject *unstructured.Unstructur
424443
425444 return false
426445}
446+
447+ // removeIgnoredFields removes the fields matched by the given ignore rules from the object.
448+ // For each rule, a SelectorRegex is compiled from the rule's Selector. If the selector matches
449+ // the object, the rule's Paths are collected and a JSON remove patch is applied to the object.
450+ func removeIgnoredFields (obj * unstructured.Unstructured , rules []jsondiff.IgnoreRule ) error {
451+ sm := make (map [* jsondiff.SelectorRegex ][]string , len (rules ))
452+ for _ , rule := range rules {
453+ sr , err := jsondiff .NewSelectorRegex (rule .Selector )
454+ if err != nil {
455+ return fmt .Errorf ("failed to create ignore rule selector: %w" , err )
456+ }
457+ sm [sr ] = rule .Paths
458+ }
459+
460+ var ignorePaths jsondiff.IgnorePaths
461+ for sr , paths := range sm {
462+ if sr .MatchUnstructured (obj ) {
463+ ignorePaths = append (ignorePaths , paths ... )
464+ }
465+ }
466+
467+ if len (ignorePaths ) > 0 {
468+ patch := jsondiff .GenerateRemovePatch (ignorePaths ... )
469+ if err := jsondiff .ApplyPatchToUnstructured (obj , patch ); err != nil {
470+ return err
471+ }
472+ }
473+
474+ return nil
475+ }
0 commit comments