-
Notifications
You must be signed in to change notification settings - Fork 46
/
filterintent.go
137 lines (120 loc) · 4.78 KB
/
filterintent.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// Copyright 2022 Lingfei Kong <colin404@foxmail.com>. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file. The original repo for
// this file is https://github.com/superproj/onex.
//
package ssa
import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/superproj/onex/internal/pkg/contract"
)
// FilterObjectInput holds info required while filtering the object.
type FilterObjectInput struct {
// AllowedPaths instruct FilterObject to ignore everything except given paths.
AllowedPaths []contract.Path
// IgnorePaths instruct FilterObject to ignore given paths.
// NOTE: IgnorePaths are used to filter out fields nested inside AllowedPaths, e.g.
// spec.ControlPlaneEndpoint.
// NOTE: ignore paths which point to an array are not supported by the current implementation.
IgnorePaths []contract.Path
}
// FilterObject filter out changes not relevant for the controller.
func FilterObject(obj *unstructured.Unstructured, input *FilterObjectInput) {
// filter out changes not in the allowed paths (fields to not consider, e.g. status);
if len(input.AllowedPaths) > 0 {
FilterIntent(&FilterIntentInput{
Path: contract.Path{},
Value: obj.Object,
ShouldFilter: IsPathNotAllowed(input.AllowedPaths),
})
}
// filter out changes for ignore paths (well known fields owned by other controllers, e.g.
// spec.controlPlaneEndpoint in the InfrastructureCluster object);
if len(input.IgnorePaths) > 0 {
FilterIntent(&FilterIntentInput{
Path: contract.Path{},
Value: obj.Object,
ShouldFilter: IsPathIgnored(input.IgnorePaths),
})
}
}
// FilterIntent ensures that object only includes the fields and values for which the controller has an opinion,
// and filter out everything else by removing it from the Value.
// NOTE: This func is called recursively only for fields of type Map, but this is ok given the current use cases
// this func has to address. More specifically, we are using this func for filtering out not allowed paths and for ignore paths;
// all of them are defined in reconcile_state.go and are targeting well-known fields inside nested maps.
// Allowed paths / ignore paths which point to an array are not supported by the current implementation.
func FilterIntent(ctx *FilterIntentInput) bool {
value, ok := ctx.Value.(map[string]any)
if !ok {
return false
}
gotDeletions := false
for field := range value {
fieldCtx := &FilterIntentInput{
// Compose the Path for the nested field.
Path: ctx.Path.Append(field),
// Gets the original and the modified Value for the field.
Value: value[field],
// Carry over global values from the context.
ShouldFilter: ctx.ShouldFilter,
}
// If the field should be filtered out, delete it from the modified object.
if fieldCtx.ShouldFilter(fieldCtx.Path) {
delete(value, field)
gotDeletions = true
continue
}
// Process nested fields and get in return if FilterIntent removed fields.
if FilterIntent(fieldCtx) {
// Ensure we are not leaving empty maps around.
if v, ok := fieldCtx.Value.(map[string]any); ok && len(v) == 0 {
delete(value, field)
gotDeletions = true
}
}
}
return gotDeletions
}
// FilterIntentInput holds info required while filtering the intent for server side apply.
// NOTE: in server side apply an intent is a partial object that only includes the fields and values for which the user has an opinion.
type FilterIntentInput struct {
// the Path of the field being processed.
Path contract.Path
// the Value for the current Path.
Value any
// ShouldFilter handle the func that determine if the current Path should be dropped or not.
ShouldFilter func(path contract.Path) bool
}
// IsPathAllowed returns true when the Path is one of the AllowedPaths.
func IsPathAllowed(allowedPaths []contract.Path) func(path contract.Path) bool {
return func(path contract.Path) bool {
for _, p := range allowedPaths {
// NOTE: we allow everything Equal or one IsParentOf one of the allowed paths.
// e.g. if allowed Path is metadata.labels, we allow both metadata and metadata.labels;
// this is required because allowed Path is called recursively.
if path.Overlaps(p) {
return true
}
}
return false
}
}
// IsPathNotAllowed returns true when the Path is NOT one of the AllowedPaths.
func IsPathNotAllowed(allowedPaths []contract.Path) func(path contract.Path) bool {
return func(path contract.Path) bool {
isAllowed := IsPathAllowed(allowedPaths)
return !isAllowed(path)
}
}
// IsPathIgnored returns true when the Path is one of the IgnorePaths.
func IsPathIgnored(ignorePaths []contract.Path) func(path contract.Path) bool {
return func(path contract.Path) bool {
for _, p := range ignorePaths {
if path.Equal(p) {
return true
}
}
return false
}
}