kubelatte-ce
Форк от sbertech/kubelatte-ce
452 строки · 12.4 Кб
1package match
2
3import (
4"context"
5"errors"
6"fmt"
7"github.com/jmespath/go-jmespath"
8"gitverse.ru/ktrntrsv/kubelatte-ce/pkg/api/common"
9"gitverse.ru/ktrntrsv/kubelatte-ce/pkg/api/v1alpha1"
10"gitverse.ru/ktrntrsv/kubelatte-ce/pkg/observability/logger"
11"gitverse.ru/ktrntrsv/kubelatte-ce/pkg/storage"
12"k8s.io/api/admission/v1beta1"
13"reflect"
14"regexp"
15"strings"
16)
17
18const Enabled = "enabled"
19
20type selOperator string
21
22const (
23In selOperator = "In"
24NotIn selOperator = "NotIn"
25Exists selOperator = "Exists"
26DoesNotExist selOperator = "DoesNotExist"
27)
28
29const (
30scopeCluster = "Cluster"
31scopeNamespaced = "Namespaced"
32scopeAny = "*"
33)
34
35const (
36mustExist = "MustExist"
37mustNotExist = "MustNotExist"
38)
39
40var ErrMatchFailed = errors.New("err match failed")
41
42type MatcherI interface {
43Match(ctx context.Context, data common.ARFields, match v1alpha1.Match) (bool, error)
44}
45
46type Matcher struct{}
47
48func NewMatcher() MatcherI {
49return &Matcher{}
50}
51
52func (m *Matcher) Match(ctx context.Context, data common.ARFields, match v1alpha1.Match) (bool, error) {
53log := logger.FromContext(ctx)
54
55if match.IsEmpty() {
56return true, nil
57}
58
59var validationFuncs []func(ctx context.Context, data common.ARFields, match v1alpha1.Match) (matches bool, reason string)
60validationFuncs = append(validationFuncs,
61m.doesOperationsMatches,
62m.doesScopeMatches,
63m.doesUserInfoMatches,
64m.doesKindsMatches,
65m.doesNameMatches,
66m.doesNamespaceSelectorMatches,
67m.doesIncludeExcludeNssMatch,
68m.doesAnnotationSelectorMatches,
69m.doesLabelSelectorMatches,
70m.doesMatchConditionsMatches,
71)
72var matches bool
73var reason string
74for _, f := range validationFuncs {
75matches, reason = f(ctx, data, match)
76
77if !matches {
78obj := getObjectToValidate(ctx, data)
79var name = getAdmissionReviewResourceName(obj)
80log.Debugf("[NotMatched] resource kind %v name %v due to %s", data.Kind.Kind, name, reason)
81return false, errors.New(reason)
82}
83}
84
85return true, nil
86}
87
88func getAdmissionReviewResourceName(obj map[string]interface{}) (name string) {
89metadata, _ := obj["metadata"].(map[string]interface{})
90if metadata["name"] != nil {
91name = metadata["name"].(string)
92} else if metadata["generateName"] != nil {
93name = metadata["generateName"].(string)
94} else {
95name = "config_name_was_not_found"
96}
97return name
98}
99
100func getObjectToValidate(ctx context.Context, data common.ARFields) map[string]interface{} {
101log := logger.FromContext(ctx)
102switch data.Operation {
103case v1beta1.Create:
104return data.Object
105case v1beta1.Update:
106return data.Object
107case v1beta1.Delete:
108return data.OldObject
109}
110
111log.Error("wrong operation")
112return nil
113}
114
115func (m *Matcher) doesOperationsMatches(ctx context.Context, fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
116mOperation := match.Operations
117arOperation := fields.Operation
118
119if len(mOperation) == 0 {
120return true, ""
121}
122
123for _, op := range mOperation {
124if v1beta1.Operation(op) == arOperation {
125return true, ""
126}
127}
128return false, "request.operation"
129}
130
131func (m *Matcher) doesScopeMatches(ctx context.Context, fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
132mScope := match.Scope
133if mScope == scopeAny || mScope == "" {
134return true, ""
135}
136var isNs bool
137isNs = isNamespaced(fields)
138if (isNs && mScope == scopeNamespaced) || (!isNs && mScope == scopeCluster) {
139return true, ""
140} else {
141return false, "request.namespace"
142}
143}
144
145func (m *Matcher) doesUserInfoMatches(ctx context.Context, fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
146if reflect.DeepEqual(match.UserInfo, v1alpha1.UserInfo{}) {
147return true, ""
148}
149mUsername := match.UserInfo.Username
150arUsername := fields.UserInfo.Username
151if mUsername == "" {
152mUsername = match.UserInfo.UsernameRegex
153b, err := regexp.Match(mUsername, []byte(arUsername))
154if err != nil {
155return false, "wrong match.userInfo.usernameRegex: " + fmt.Sprint(err)
156}
157if !b {
158return false, "request.userInfo.username"
159}
160return b, ""
161} else {
162if match.UserInfo.Username != arUsername {
163return false, "request.userInfo.username"
164}
165
166return true, ""
167}
168}
169
170func (m *Matcher) doesKindsMatches(ctx context.Context, fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
171mKinds := match.Kinds
172arKinds := fields.Kind
173
174kMatch, vMatch := false, false
175
176for _, kind := range mKinds {
177if !kMatch {
178kMatch = existsStringInSlice(kind.Kind, arKinds.Kind)
179}
180if !vMatch {
181vMatch = checkStringsMatchingWithSliceRegexp(kind.ApiGroups, arKinds.Group)
182}
183}
184if kMatch && vMatch {
185return true, ""
186}
187
188if !kMatch {
189reason = "request.kind.kind"
190}
191if !vMatch {
192reason = "request.kind.group"
193}
194
195return false, reason
196}
197
198func (m *Matcher) doesNameMatches(ctx context.Context, fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
199reason = "request.oldObject.metadata.name"
200
201namePattern := match.Name
202if reflect.DeepEqual(namePattern, v1alpha1.Name{}) { //nolint:exhaustruct
203return true, ""
204}
205object := getObjectToValidate(ctx, fields)
206name := getAdmissionReviewResourceName(object)
207
208if namePattern.Value != "" {
209if namePattern.Value == name {
210return true, ""
211} else {
212return false, reason
213}
214} else {
215matchRes, err := regexp.Match(namePattern.Regex, []byte(name))
216if err != nil {
217return false, "wrong match.name.regex: " + fmt.Sprint(err)
218}
219return matchRes, reason
220}
221}
222
223func (m *Matcher) doesIncludeExcludeNssMatch(ctx context.Context, fields common.ARFields, match v1alpha1.Match) (bool, string) {
224
225var matches = true
226var reason = ""
227
228var excluded = match.ExcludedNamespaces
229var included = match.IncludedNamespaces
230
231ns := fields.Namespace
232if ns == "" {
233return true, ""
234}
235
236if !reflect.DeepEqual(excluded, v1alpha1.Namespace{Values: nil, Regex: nil}) {
237matches, reason = m.doesNsNamesMatch(excluded, ns, false)
238if !matches {
239return matches, reason
240}
241} else if !reflect.DeepEqual(included, v1alpha1.Namespace{Values: nil, Regex: nil}) {
242matches, reason = m.doesNsNamesMatch(included, ns, true)
243if !matches {
244return matches, reason
245}
246}
247
248return true, ""
249}
250
251func (m *Matcher) doesNsNamesMatch(matchNs v1alpha1.Namespace, ns string, include bool) (bool, string) {
252
253var reason = ""
254var reasonPrefix string
255var match bool
256
257if include {
258reasonPrefix = "included "
259} else {
260reasonPrefix = "excluded "
261}
262
263if matchNs.Values != nil {
264match, reason = findNonRegexNsMatch(ns, matchNs.Values)
265return include == match, reasonPrefix + reason
266} else {
267match, reason = findRegexNsMatch(ns, matchNs.Regex)
268if reason != "namespace invalid regex" {
269return include == match, reasonPrefix + reason
270} else {
271return match, reason
272}
273
274}
275}
276
277func findNonRegexNsMatch(ns string, matchVal []string) (bool, string) {
278for _, matchNs := range matchVal {
279if ns == matchNs {
280return true, "namespace value"
281}
282}
283return false, "namespace value"
284}
285
286func findRegexNsMatch(ns string, matchVal []string) (bool, string) {
287log := logger.FromContext(context.Background())
288for _, matchNs := range matchVal {
289match, err := regexp.Match(matchNs, []byte(ns))
290if err != nil {
291log.Errorf("Namespace regex err: %s", err.Error())
292return false, "namespace invalid regex"
293}
294
295if match {
296return true, "namespace regex"
297}
298}
299return false, "namespace regex"
300}
301
302func (m *Matcher) doesNamespaceSelectorMatches(ctx context.Context, fields common.ARFields, match v1alpha1.Match) (bool, string) {
303log := logger.FromContext(ctx)
304if reflect.DeepEqual(match.NamespaceSelector, v1alpha1.NamespaceSelector{}) {
305return true, "namespace selector"
306}
307
308if fields.Namespace == "" { // non namespaced object
309log.Debugf("[Skipped selector namespace] resource %s escaped NamespaceSelectorMatch check because it's Cluster Object", fields.Kind.Kind)
310return true, "namespace selector"
311}
312
313namespaces := storage.Storage.GetNamespaces()
314for _, ns := range namespaces {
315if ns.ObjectMeta.Name == fields.Namespace {
316matches := doesMatchExpressionsMatch(ns.ObjectMeta.Labels, match.NamespaceSelector.MatchExpressions)
317if matches {
318return true, ""
319}
320}
321}
322return false, "namespace selector"
323}
324
325func (m *Matcher) doesAnnotationSelectorMatches(ctx context.Context, fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
326log := logger.FromContext(ctx)
327if reflect.DeepEqual(match.AnnotationSelector, v1alpha1.AnnotationSelector{}) {
328return true, ""
329}
330matchExpr := match.AnnotationSelector.MatchExpressions
331
332object := getObjectToValidate(ctx, fields)
333annotations, _ := jmespath.Search("metadata.annotations", object)
334
335arAnnotations, ok := annotations.(map[string]interface{})
336if !ok {
337log.Info("perhaps the configuration being processed does not have annotations. " +
338"an error was detected when trying to extract the metadata.annotations section")
339return false, "request.metadata.annotations"
340}
341arAnnotationsStr := parseMapValuesToString(arAnnotations)
342
343return doesMatchExpressionsMatch(arAnnotationsStr, matchExpr), "annotation selector"
344}
345
346func (m *Matcher) doesLabelSelectorMatches(ctx context.Context, fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
347log := logger.FromContext(ctx)
348selector := match.LabelSelector
349object := getObjectToValidate(ctx, fields)
350
351if reflect.DeepEqual(selector, v1alpha1.LabelSelector{}) {
352return true, ""
353}
354
355labels, _ := jmespath.Search("metadata.labels", object)
356
357lbs, ok := labels.(map[string]interface{})
358if !ok {
359log.Info("perhaps the configuration being processed does not have labels. " +
360"an error was detected when trying to extract the metadata.labels section")
361return false, "wrong metadata.labels format"
362}
363arLabelsStr := parseMapValuesToString(lbs)
364
365return doesMatchExpressionsMatch(arLabelsStr, selector.MatchExpressions), "metadata.labels"
366}
367
368func (m *Matcher) doesMatchConditionsMatches(ctx context.Context, fields common.ARFields, match v1alpha1.Match) (bool, string) {
369if reflect.DeepEqual(match.MatchConditions, v1alpha1.MatchConditions{}) {
370return true, ""
371}
372
373var matches = true
374var reason = ""
375
376switch fields.Operation {
377case v1beta1.Create:
378if fields.Object != nil && len(match.MatchConditions.Object) != 0 {
379matches, reason = m.doesObjectSectionMatches(fields.Object, match.MatchConditions.Object)
380if !matches {
381return matches, reason
382}
383}
384case v1beta1.Delete:
385if fields.OldObject != nil && len(match.MatchConditions.OldObject) != 0 {
386matches, reason = m.doesObjectSectionMatches(fields.OldObject, match.MatchConditions.OldObject)
387if !matches {
388return matches, reason
389}
390}
391case v1beta1.Update:
392if fields.Object != nil && len(match.MatchConditions.Object) != 0 {
393matches, reason = m.doesObjectSectionMatches(fields.Object, match.MatchConditions.Object)
394if !matches {
395return matches, reason
396}
397}
398if fields.OldObject != nil && len(match.MatchConditions.OldObject) != 0 {
399matches, reason = m.doesObjectSectionMatches(fields.OldObject, match.MatchConditions.OldObject)
400if !matches {
401return matches, reason
402}
403}
404}
405return true, ""
406
407}
408
409func (m *Matcher) doesObjectSectionMatches(obj map[string]interface{}, matchObjs []v1alpha1.Obj) (bool, string) {
410log := logger.FromContext(context.Background())
411if len(matchObjs) == 0 {
412return false, "match conditions doesn't have enough of arguments"
413}
414for _, mObj := range matchObjs {
415var path = strings.ReplaceAll(mObj.Path, "`", "'")
416res, err := jmespath.Search(path, obj)
417if err != nil {
418log.Errorf("when processing MatchConditions jmespath: %s, value: %s an error was detected: %s ", mObj.Path, mObj.Condition, err.Error())
419return false, "match conditions jmespath processing error"
420}
421switch v := res.(type) {
422case []interface{}:
423if (len(v) == 0 && mObj.Condition == mustNotExist) || (len(v) != 0 && mObj.Condition == mustExist) {
424continue
425}
426return false, "match conditions"
427case bool:
428if (v == true && mObj.Condition == mustExist) || (v == false && mObj.Condition == mustNotExist) {
429continue
430}
431return false, "match conditions"
432case interface{}:
433if (v == nil && mObj.Condition == mustNotExist) || (v != nil && mObj.Condition == mustExist) {
434continue
435}
436return false, "match conditions"
437default:
438if v == nil {
439if mObj.Condition == mustNotExist {
440continue
441}
442return false, "match conditions"
443}
444
445log.Errorf("when processing MatchConditions jmespath: %s, value: %s an error occurred with unexpected type: %T! ", mObj.Path, mObj.Condition, v)
446return false, "match conditions"
447
448}
449
450}
451return true, ""
452}
453