kubelatte-ce

Форк
2
Форк от sbertech/kubelatte-ce
/
matcher.go 
452 строки · 12.4 Кб
1
package match
2

3
import (
4
	"context"
5
	"errors"
6
	"fmt"
7
	"github.com/jmespath/go-jmespath"
8
	"gitverse.ru/synapse/kubelatte/pkg/api/common"
9
	"gitverse.ru/synapse/kubelatte/pkg/api/v1alpha1"
10
	"gitverse.ru/synapse/kubelatte/pkg/observability/logger"
11
	"gitverse.ru/synapse/kubelatte/pkg/storage"
12
	"k8s.io/api/admission/v1beta1"
13
	"reflect"
14
	"regexp"
15
	"strings"
16
)
17

18
const Enabled = "enabled"
19

20
type selOperator string
21

22
const (
23
	In           selOperator = "In"
24
	NotIn        selOperator = "NotIn"
25
	Exists       selOperator = "Exists"
26
	DoesNotExist selOperator = "DoesNotExist"
27
)
28

29
const (
30
	scopeCluster    = "Cluster"
31
	scopeNamespaced = "Namespaced"
32
	scopeAny        = "*"
33
)
34

35
const (
36
	mustExist    = "MustExist"
37
	mustNotExist = "MustNotExist"
38
)
39

40
var ErrMatchFailed = errors.New("err match failed")
41

42
type MatcherI interface {
43
	Match(ctx context.Context, data common.ARFields, match v1alpha1.Match) (bool, error)
44
}
45

46
type Matcher struct{}
47

48
func NewMatcher() MatcherI {
49
	return &Matcher{}
50
}
51

52
func (m *Matcher) Match(ctx context.Context, data common.ARFields, match v1alpha1.Match) (bool, error) {
53
	log := logger.FromContext(ctx)
54

55
	if match.IsEmpty() {
56
		return true, nil
57
	}
58

59
	var validationFuncs []func(ctx context.Context, data common.ARFields, match v1alpha1.Match) (matches bool, reason string)
60
	validationFuncs = append(validationFuncs,
61
		m.doesOperationsMatches,
62
		m.doesScopeMatches,
63
		m.doesUserInfoMatches,
64
		m.doesKindsMatches,
65
		m.doesNameMatches,
66
		m.doesNamespaceSelectorMatches,
67
		m.doesIncludeExcludeNssMatch,
68
		m.doesAnnotationSelectorMatches,
69
		m.doesLabelSelectorMatches,
70
		m.doesMatchConditionsMatches,
71
	)
72
	var matches bool
73
	var reason string
74
	for _, f := range validationFuncs {
75
		matches, reason = f(ctx, data, match)
76

77
		if !matches {
78
			obj := getObjectToValidate(ctx, data)
79
			var name = getAdmissionReviewResourceName(obj)
80
			log.Debugf("[NotMatched] resource kind %v name %v due to %s", data.Kind.Kind, name, reason)
81
			return false, errors.New(reason)
82
		}
83
	}
84

85
	return true, nil
86
}
87

88
func getAdmissionReviewResourceName(obj map[string]interface{}) (name string) {
89
	metadata, _ := obj["metadata"].(map[string]interface{})
90
	if metadata["name"] != nil {
91
		name = metadata["name"].(string)
92
	} else if metadata["generateName"] != nil {
93
		name = metadata["generateName"].(string)
94
	} else {
95
		name = "config_name_was_not_found"
96
	}
97
	return name
98
}
99

100
func getObjectToValidate(ctx context.Context, data common.ARFields) map[string]interface{} {
101
	log := logger.FromContext(ctx)
102
	switch data.Operation {
103
	case v1beta1.Create:
104
		return data.Object
105
	case v1beta1.Update:
106
		return data.Object
107
	case v1beta1.Delete:
108
		return data.OldObject
109
	}
110

111
	log.Error("wrong operation")
112
	return nil
113
}
114

115
func (m *Matcher) doesOperationsMatches(ctx context.Context, fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
116
	mOperation := match.Operations
117
	arOperation := fields.Operation
118

119
	if len(mOperation) == 0 {
120
		return true, ""
121
	}
122

123
	for _, op := range mOperation {
124
		if v1beta1.Operation(op) == arOperation {
125
			return true, ""
126
		}
127
	}
128
	return false, "request.operation"
129
}
130

131
func (m *Matcher) doesScopeMatches(ctx context.Context, fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
132
	mScope := match.Scope
133
	if mScope == scopeAny || mScope == "" {
134
		return true, ""
135
	}
136
	var isNs bool
137
	isNs = isNamespaced(fields)
138
	if (isNs && mScope == scopeNamespaced) || (!isNs && mScope == scopeCluster) {
139
		return true, ""
140
	} else {
141
		return false, "request.namespace"
142
	}
143
}
144

145
func (m *Matcher) doesUserInfoMatches(ctx context.Context, fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
146
	if reflect.DeepEqual(match.UserInfo, v1alpha1.UserInfo{}) {
147
		return true, ""
148
	}
149
	mUsername := match.UserInfo.Username
150
	arUsername := fields.UserInfo.Username
151
	if mUsername == "" {
152
		mUsername = match.UserInfo.UsernameRegex
153
		b, err := regexp.Match(mUsername, []byte(arUsername))
154
		if err != nil {
155
			return false, "wrong match.userInfo.usernameRegex: " + fmt.Sprint(err)
156
		}
157
		if !b {
158
			return false, "request.userInfo.username"
159
		}
160
		return b, ""
161
	} else {
162
		if match.UserInfo.Username != arUsername {
163
			return false, "request.userInfo.username"
164
		}
165

166
		return true, ""
167
	}
168
}
169

170
func (m *Matcher) doesKindsMatches(ctx context.Context, fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
171
	mKinds := match.Kinds
172
	arKinds := fields.Kind
173

174
	kMatch, vMatch := false, false
175

176
	for _, kind := range mKinds {
177
		if !kMatch {
178
			kMatch = existsStringInSlice(kind.Kind, arKinds.Kind)
179
		}
180
		if !vMatch {
181
			vMatch = checkStringsMatchingWithSliceRegexp(kind.ApiGroups, arKinds.Group)
182
		}
183
	}
184
	if kMatch && vMatch {
185
		return true, ""
186
	}
187

188
	if !kMatch {
189
		reason = "request.kind.kind"
190
	}
191
	if !vMatch {
192
		reason = "request.kind.group"
193
	}
194

195
	return false, reason
196
}
197

198
func (m *Matcher) doesNameMatches(ctx context.Context, fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
199
	reason = "request.oldObject.metadata.name"
200

201
	namePattern := match.Name
202
	if reflect.DeepEqual(namePattern, v1alpha1.Name{}) { //nolint:exhaustruct
203
		return true, ""
204
	}
205
	object := getObjectToValidate(ctx, fields)
206
	name := getAdmissionReviewResourceName(object)
207

208
	if namePattern.Value != "" {
209
		if namePattern.Value == name {
210
			return true, ""
211
		} else {
212
			return false, reason
213
		}
214
	} else {
215
		matchRes, err := regexp.Match(namePattern.Regex, []byte(name))
216
		if err != nil {
217
			return false, "wrong match.name.regex: " + fmt.Sprint(err)
218
		}
219
		return matchRes, reason
220
	}
221
}
222

223
func (m *Matcher) doesIncludeExcludeNssMatch(ctx context.Context, fields common.ARFields, match v1alpha1.Match) (bool, string) {
224

225
	var matches = true
226
	var reason = ""
227

228
	var excluded = match.ExcludedNamespaces
229
	var included = match.IncludedNamespaces
230

231
	ns := fields.Namespace
232
	if ns == "" {
233
		return true, ""
234
	}
235

236
	if !reflect.DeepEqual(excluded, v1alpha1.Namespace{Values: nil, Regex: nil}) {
237
		matches, reason = m.doesNsNamesMatch(excluded, ns, false)
238
		if !matches {
239
			return matches, reason
240
		}
241
	} else if !reflect.DeepEqual(included, v1alpha1.Namespace{Values: nil, Regex: nil}) {
242
		matches, reason = m.doesNsNamesMatch(included, ns, true)
243
		if !matches {
244
			return matches, reason
245
		}
246
	}
247

248
	return true, ""
249
}
250

251
func (m *Matcher) doesNsNamesMatch(matchNs v1alpha1.Namespace, ns string, include bool) (bool, string) {
252

253
	var reason = ""
254
	var reasonPrefix string
255
	var match bool
256

257
	if include {
258
		reasonPrefix = "included "
259
	} else {
260
		reasonPrefix = "excluded "
261
	}
262

263
	if matchNs.Values != nil {
264
		match, reason = findNonRegexNsMatch(ns, matchNs.Values)
265
		return include == match, reasonPrefix + reason
266
	} else {
267
		match, reason = findRegexNsMatch(ns, matchNs.Regex)
268
		if reason != "namespace invalid regex" {
269
			return include == match, reasonPrefix + reason
270
		} else {
271
			return match, reason
272
		}
273

274
	}
275
}
276

277
func findNonRegexNsMatch(ns string, matchVal []string) (bool, string) {
278
	for _, matchNs := range matchVal {
279
		if ns == matchNs {
280
			return true, "namespace value"
281
		}
282
	}
283
	return false, "namespace value"
284
}
285

286
func findRegexNsMatch(ns string, matchVal []string) (bool, string) {
287
	log := logger.FromContext(context.Background())
288
	for _, matchNs := range matchVal {
289
		match, err := regexp.Match(matchNs, []byte(ns))
290
		if err != nil {
291
			log.Errorf("Namespace regex err: %s", err.Error())
292
			return false, "namespace invalid regex"
293
		}
294

295
		if match {
296
			return true, "namespace regex"
297
		}
298
	}
299
	return false, "namespace regex"
300
}
301

302
func (m *Matcher) doesNamespaceSelectorMatches(ctx context.Context, fields common.ARFields, match v1alpha1.Match) (bool, string) {
303
	log := logger.FromContext(ctx)
304
	if reflect.DeepEqual(match.NamespaceSelector, v1alpha1.NamespaceSelector{}) {
305
		return true, "namespace selector"
306
	}
307

308
	if fields.Namespace == "" { // non namespaced object
309
		log.Debugf("[Skipped selector namespace] resource %s escaped NamespaceSelectorMatch check because it's Cluster Object", fields.Kind.Kind)
310
		return true, "namespace selector"
311
	}
312

313
	namespaces := storage.Storage.GetNamespaces()
314
	for _, ns := range namespaces {
315
		if ns.ObjectMeta.Name == fields.Namespace {
316
			matches := doesMatchExpressionsMatch(ns.ObjectMeta.Labels, match.NamespaceSelector.MatchExpressions)
317
			if matches {
318
				return true, ""
319
			}
320
		}
321
	}
322
	return false, "namespace selector"
323
}
324

325
func (m *Matcher) doesAnnotationSelectorMatches(ctx context.Context, fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
326
	log := logger.FromContext(ctx)
327
	if reflect.DeepEqual(match.AnnotationSelector, v1alpha1.AnnotationSelector{}) {
328
		return true, ""
329
	}
330
	matchExpr := match.AnnotationSelector.MatchExpressions
331

332
	object := getObjectToValidate(ctx, fields)
333
	annotations, _ := jmespath.Search("metadata.annotations", object)
334

335
	arAnnotations, ok := annotations.(map[string]interface{})
336
	if !ok {
337
		log.Info("perhaps the configuration being processed does not have annotations. " +
338
			"an error was detected when trying to extract the metadata.annotations section")
339
		return false, "request.metadata.annotations"
340
	}
341
	arAnnotationsStr := parseMapValuesToString(arAnnotations)
342

343
	return doesMatchExpressionsMatch(arAnnotationsStr, matchExpr), "annotation selector"
344
}
345

346
func (m *Matcher) doesLabelSelectorMatches(ctx context.Context, fields common.ARFields, match v1alpha1.Match) (matches bool, reason string) {
347
	log := logger.FromContext(ctx)
348
	selector := match.LabelSelector
349
	object := getObjectToValidate(ctx, fields)
350

351
	if reflect.DeepEqual(selector, v1alpha1.LabelSelector{}) {
352
		return true, ""
353
	}
354

355
	labels, _ := jmespath.Search("metadata.labels", object)
356

357
	lbs, ok := labels.(map[string]interface{})
358
	if !ok {
359
		log.Info("perhaps the configuration being processed does not have labels. " +
360
			"an error was detected when trying to extract the metadata.labels section")
361
		return false, "wrong metadata.labels format"
362
	}
363
	arLabelsStr := parseMapValuesToString(lbs)
364

365
	return doesMatchExpressionsMatch(arLabelsStr, selector.MatchExpressions), "metadata.labels"
366
}
367

368
func (m *Matcher) doesMatchConditionsMatches(ctx context.Context, fields common.ARFields, match v1alpha1.Match) (bool, string) {
369
	if reflect.DeepEqual(match.MatchConditions, v1alpha1.MatchConditions{}) {
370
		return true, ""
371
	}
372

373
	var matches = true
374
	var reason = ""
375

376
	switch fields.Operation {
377
	case v1beta1.Create:
378
		if fields.Object != nil && len(match.MatchConditions.Object) != 0 {
379
			matches, reason = m.doesObjectSectionMatches(fields.Object, match.MatchConditions.Object)
380
			if !matches {
381
				return matches, reason
382
			}
383
		}
384
	case v1beta1.Delete:
385
		if fields.OldObject != nil && len(match.MatchConditions.OldObject) != 0 {
386
			matches, reason = m.doesObjectSectionMatches(fields.OldObject, match.MatchConditions.OldObject)
387
			if !matches {
388
				return matches, reason
389
			}
390
		}
391
	case v1beta1.Update:
392
		if fields.Object != nil && len(match.MatchConditions.Object) != 0 {
393
			matches, reason = m.doesObjectSectionMatches(fields.Object, match.MatchConditions.Object)
394
			if !matches {
395
				return matches, reason
396
			}
397
		}
398
		if fields.OldObject != nil && len(match.MatchConditions.OldObject) != 0 {
399
			matches, reason = m.doesObjectSectionMatches(fields.OldObject, match.MatchConditions.OldObject)
400
			if !matches {
401
				return matches, reason
402
			}
403
		}
404
	}
405
	return true, ""
406

407
}
408

409
func (m *Matcher) doesObjectSectionMatches(obj map[string]interface{}, matchObjs []v1alpha1.Obj) (bool, string) {
410
	log := logger.FromContext(context.Background())
411
	if len(matchObjs) == 0 {
412
		return false, "match conditions doesn't have enough of arguments"
413
	}
414
	for _, mObj := range matchObjs {
415
		var path = strings.ReplaceAll(mObj.Path, "`", "'")
416
		res, err := jmespath.Search(path, obj)
417
		if err != nil {
418
			log.Errorf("when processing MatchConditions jmespath: %s, value: %s an error was detected: %s ", mObj.Path, mObj.Condition, err.Error())
419
			return false, "match conditions jmespath processing error"
420
		}
421
		switch v := res.(type) {
422
		case []interface{}:
423
			if (len(v) == 0 && mObj.Condition == mustNotExist) || (len(v) != 0 && mObj.Condition == mustExist) {
424
				continue
425
			}
426
			return false, "match conditions"
427
		case bool:
428
			if (v == true && mObj.Condition == mustExist) || (v == false && mObj.Condition == mustNotExist) {
429
				continue
430
			}
431
			return false, "match conditions"
432
		case interface{}:
433
			if (v == nil && mObj.Condition == mustNotExist) || (v != nil && mObj.Condition == mustExist) {
434
				continue
435
			}
436
			return false, "match conditions"
437
		default:
438
			if v == nil {
439
				if mObj.Condition == mustNotExist {
440
					continue
441
				}
442
				return false, "match conditions"
443
			}
444

445
			log.Errorf("when processing MatchConditions jmespath: %s, value: %s an error occurred with unexpected type: %T! ", mObj.Path, mObj.Condition, v)
446
			return false, "match conditions"
447

448
		}
449

450
	}
451
	return true, ""
452
}
453

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.