1 // Copyright 2021 The Kubernetes Authors. 2 // SPDX-License-Identifier: Apache-2.0 3 4 package framework 5 6 import ( 7 "bytes" 8 "strings" 9 "text/template" 10 11 "sigs.k8s.io/kustomize/kyaml/errors" 12 "sigs.k8s.io/kustomize/kyaml/kio" 13 "sigs.k8s.io/kustomize/kyaml/sets" 14 "sigs.k8s.io/kustomize/kyaml/yaml" 15 ) 16 17 // ResourceMatcher is implemented by types designed for use in or as selectors. 18 type ResourceMatcher interface { 19 // kio.Filter applies the matcher to multiple resources. 20 // This makes individual matchers usable as selectors directly. 21 kio.Filter 22 // Match returns true if the given resource matches the matcher's configuration. 23 Match(node *yaml.RNode) bool 24 } 25 26 // ResourceMatcherFunc converts a compliant function into a ResourceMatcher 27 type ResourceMatcherFunc func(node *yaml.RNode) bool 28 29 // Match runs the ResourceMatcherFunc on the given node. 30 func (m ResourceMatcherFunc) Match(node *yaml.RNode) bool { 31 return m(node) 32 } 33 34 // Filter applies ResourceMatcherFunc to a list of items, returning only those that match. 35 func (m ResourceMatcherFunc) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) { 36 // MatchAll or MatchAny doesn't really matter here since there is only one matcher (m). 37 return MatchAll(m).Filter(items) 38 } 39 40 // ResourceTemplateMatcher is implemented by ResourceMatcher types that accept text templates as 41 // part of their configuration. 42 type ResourceTemplateMatcher interface { 43 // ResourceMatcher makes matchers usable in or as selectors. 44 ResourceMatcher 45 // DefaultTemplateData is used to pass default template values down a chain of matchers. 46 DefaultTemplateData(interface{}) 47 // InitTemplates is used to render the templates in selectors that support 48 // ResourceTemplateMatcher. The selector should call this exactly once per filter 49 // operation, before beginning match comparisons. 50 InitTemplates() error 51 } 52 53 // ContainerNameMatcher returns a function that returns true if the "name" field 54 // of the provided container node matches one of the given container names. 55 // If no names are provided, the function always returns true. 56 // Note that this is not a ResourceMatcher, since the node it matches against must be 57 // container-level (e.g. "name", "env" and "image" would be top level fields). 58 func ContainerNameMatcher(names ...string) func(node *yaml.RNode) bool { 59 namesSet := sets.String{} 60 namesSet.Insert(names...) 61 return func(node *yaml.RNode) bool { 62 if len(namesSet) == 0 { 63 return true 64 } 65 f := node.Field("name") 66 if f == nil { 67 return false 68 } 69 return namesSet.Has(yaml.GetValue(f.Value)) 70 } 71 } 72 73 // NameMatcher matches resources whose metadata.name is equal to one of the provided values. 74 // e.g. `NameMatcher("foo", "bar")` matches if `metadata.name` is either "foo" or "bar". 75 // 76 // NameMatcher supports templating. 77 // e.g. `NameMatcher("{{.AppName}}")` will match `metadata.name` "foo" if TemplateData is 78 // `struct{ AppName string }{ AppName: "foo" }` 79 func NameMatcher(names ...string) ResourceTemplateMatcher { 80 return &TemplatedMetaSliceMatcher{ 81 Templates: names, 82 MetaMatcher: func(names sets.String, meta yaml.ResourceMeta) bool { 83 return names.Has(meta.Name) 84 }, 85 } 86 } 87 88 // NamespaceMatcher matches resources whose metadata.namespace is equal to one of the provided values. 89 // e.g. `NamespaceMatcher("foo", "bar")` matches if `metadata.namespace` is either "foo" or "bar". 90 // 91 // NamespaceMatcher supports templating. 92 // e.g. `NamespaceMatcher("{{.AppName}}")` will match `metadata.namespace` "foo" if TemplateData is 93 // `struct{ AppName string }{ AppName: "foo" }` 94 func NamespaceMatcher(names ...string) ResourceTemplateMatcher { 95 return &TemplatedMetaSliceMatcher{ 96 Templates: names, 97 MetaMatcher: func(names sets.String, meta yaml.ResourceMeta) bool { 98 return names.Has(meta.Namespace) 99 }, 100 } 101 } 102 103 // KindMatcher matches resources whose kind is equal to one of the provided values. 104 // e.g. `KindMatcher("foo", "bar")` matches if `kind` is either "foo" or "bar". 105 // 106 // KindMatcher supports templating. 107 // e.g. `KindMatcher("{{.TargetKind}}")` will match `kind` "foo" if TemplateData is 108 // `struct{ TargetKind string }{ TargetKind: "foo" }` 109 func KindMatcher(names ...string) ResourceTemplateMatcher { 110 return &TemplatedMetaSliceMatcher{ 111 Templates: names, 112 MetaMatcher: func(names sets.String, meta yaml.ResourceMeta) bool { 113 return names.Has(meta.Kind) 114 }, 115 } 116 } 117 118 // APIVersionMatcher matches resources whose kind is equal to one of the provided values. 119 // e.g. `APIVersionMatcher("foo/v1", "bar/v1")` matches if `apiVersion` is either "foo/v1" or 120 // "bar/v1". 121 // 122 // APIVersionMatcher supports templating. 123 // e.g. `APIVersionMatcher("{{.TargetAPI}}")` will match `apiVersion` "foo/v1" if TemplateData is 124 // `struct{ TargetAPI string }{ TargetAPI: "foo/v1" }` 125 func APIVersionMatcher(names ...string) ResourceTemplateMatcher { 126 return &TemplatedMetaSliceMatcher{ 127 Templates: names, 128 MetaMatcher: func(names sets.String, meta yaml.ResourceMeta) bool { 129 return names.Has(meta.APIVersion) 130 }, 131 } 132 } 133 134 // GVKMatcher matches resources whose API group, version and kind match one of the provided values. 135 // e.g. `GVKMatcher("foo/v1/Widget", "bar/v1/App")` matches if `apiVersion` concatenated with `kind` 136 // is either "foo/v1/Widget" or "bar/v1/App". 137 // 138 // GVKMatcher supports templating. 139 // e.g. `GVKMatcher("{{.TargetAPI}}")` will match "foo/v1/Widget" if TemplateData is 140 // `struct{ TargetAPI string }{ TargetAPI: "foo/v1/Widget" }` 141 func GVKMatcher(names ...string) ResourceTemplateMatcher { 142 return &TemplatedMetaSliceMatcher{ 143 Templates: names, 144 MetaMatcher: func(names sets.String, meta yaml.ResourceMeta) bool { 145 gvk := strings.Join([]string{meta.APIVersion, meta.Kind}, "/") 146 return names.Has(gvk) 147 }, 148 } 149 } 150 151 // TemplatedMetaSliceMatcher is a utility type for constructing matchers that compare resource 152 // metadata to a slice of (possibly templated) strings. 153 type TemplatedMetaSliceMatcher struct { 154 // Templates is the list of possibly templated strings to compare to. 155 Templates []string 156 // values is the set of final (possibly rendered) strings to compare to. 157 values sets.String 158 // TemplateData is the data to use in template rendering. 159 // Rendering will not take place if it is nil when InitTemplates is called. 160 TemplateData interface{} 161 // MetaMatcher is a function that returns true if the given resource metadata matches at 162 // least one of the given names. 163 // The matcher implemented using TemplatedMetaSliceMatcher can compare names to any meta field. 164 MetaMatcher func(names sets.String, meta yaml.ResourceMeta) bool 165 } 166 167 // Match parses the resource node's metadata and delegates matching logic to the provided 168 // MetaMatcher func. This allows ResourceMatchers build with TemplatedMetaSliceMatcher to match 169 // against any field in resource metadata. 170 func (m *TemplatedMetaSliceMatcher) Match(node *yaml.RNode) bool { 171 var err error 172 meta, err := node.GetMeta() 173 if err != nil { 174 return false 175 } 176 return m.MetaMatcher(m.values, meta) 177 } 178 179 // Filter applies the matcher to a list of items, returning only those that match. 180 func (m *TemplatedMetaSliceMatcher) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) { 181 // AndSelector or OrSelector doesn't really matter here since there is only one matcher (m). 182 s := AndSelector{Matchers: []ResourceMatcher{m}, TemplateData: m.TemplateData} 183 return s.Filter(items) 184 } 185 186 // DefaultTemplateData sets TemplateData to the provided default values if it has not already 187 // been set. 188 func (m *TemplatedMetaSliceMatcher) DefaultTemplateData(data interface{}) { 189 if m.TemplateData == nil { 190 m.TemplateData = data 191 } 192 } 193 194 // InitTemplates is used to render any templates the selector's list of strings may contain 195 // before the selector is applied. It should be called exactly once per filter 196 // operation, before beginning match comparisons. 197 func (m *TemplatedMetaSliceMatcher) InitTemplates() error { 198 values, err := templatizeSlice(m.Templates, m.TemplateData) 199 if err != nil { 200 return errors.Wrap(err) 201 } 202 m.values = sets.String{} 203 m.values.Insert(values...) 204 return nil 205 } 206 207 var _ ResourceTemplateMatcher = &TemplatedMetaSliceMatcher{} 208 209 // LabelMatcher matches resources that are labelled with all of the provided key-value pairs. 210 // e.g. `LabelMatcher(map[string]string{"app": "foo", "env": "prod"})` matches resources labelled 211 // app=foo AND env=prod. 212 // 213 // LabelMatcher supports templating. 214 // e.g. `LabelMatcher(map[string]string{"app": "{{ .AppName}}"})` will match label app=foo if 215 // TemplateData is `struct{ AppName string }{ AppName: "foo" }` 216 func LabelMatcher(labels map[string]string) ResourceTemplateMatcher { 217 return &TemplatedMetaMapMatcher{ 218 Templates: labels, 219 MetaMatcher: func(labels map[string]string, meta yaml.ResourceMeta) bool { 220 return compareMaps(labels, meta.Labels) 221 }, 222 } 223 } 224 225 func compareMaps(desired, actual map[string]string) bool { 226 for k := range desired { 227 // actual either doesn't have the key or has the wrong value for it 228 if actual[k] != desired[k] { 229 return false 230 } 231 } 232 return true 233 } 234 235 // AnnotationMatcher matches resources that are annotated with all of the provided key-value pairs. 236 // e.g. `AnnotationMatcher(map[string]string{"app": "foo", "env": "prod"})` matches resources 237 // annotated app=foo AND env=prod. 238 // 239 // AnnotationMatcher supports templating. 240 // e.g. `AnnotationMatcher(map[string]string{"app": "{{ .AppName}}"})` will match label app=foo if 241 // TemplateData is `struct{ AppName string }{ AppName: "foo" }` 242 func AnnotationMatcher(ann map[string]string) ResourceTemplateMatcher { 243 return &TemplatedMetaMapMatcher{ 244 Templates: ann, 245 MetaMatcher: func(ann map[string]string, meta yaml.ResourceMeta) bool { 246 return compareMaps(ann, meta.Annotations) 247 }, 248 } 249 } 250 251 // TemplatedMetaMapMatcher is a utility type for constructing matchers that compare resource 252 // metadata to a map of (possibly templated) key-value pairs. 253 type TemplatedMetaMapMatcher struct { 254 // Templates is the list of possibly templated strings to compare to. 255 Templates map[string]string 256 // values is the map of final (possibly rendered) strings to compare to. 257 values map[string]string 258 // TemplateData is the data to use in template rendering. 259 // Rendering will not take place if it is nil when InitTemplates is called. 260 TemplateData interface{} 261 // MetaMatcher is a function that returns true if the given resource metadata matches at 262 // least one of the given names. 263 // The matcher implemented using TemplatedMetaSliceMatcher can compare names to any meta field. 264 MetaMatcher func(names map[string]string, meta yaml.ResourceMeta) bool 265 } 266 267 // Match parses the resource node's metadata and delegates matching logic to the provided 268 // MetaMatcher func. This allows ResourceMatchers build with TemplatedMetaMapMatcher to match 269 // against any field in resource metadata. 270 func (m *TemplatedMetaMapMatcher) Match(node *yaml.RNode) bool { 271 var err error 272 meta, err := node.GetMeta() 273 if err != nil { 274 return false 275 } 276 277 return m.MetaMatcher(m.values, meta) 278 } 279 280 // DefaultTemplateData sets TemplateData to the provided default values if it has not already 281 // been set. 282 func (m *TemplatedMetaMapMatcher) DefaultTemplateData(data interface{}) { 283 if m.TemplateData == nil { 284 m.TemplateData = data 285 } 286 } 287 288 // InitTemplates is used to render any templates the selector's key-value pairs may contain 289 // before the selector is applied. It should be called exactly once per filter 290 // operation, before beginning match comparisons. 291 func (m *TemplatedMetaMapMatcher) InitTemplates() error { 292 var err error 293 m.values, err = templatizeMap(m.Templates, m.TemplateData) 294 return errors.Wrap(err) 295 } 296 297 // Filter applies the matcher to a list of items, returning only those that match. 298 func (m *TemplatedMetaMapMatcher) Filter(items []*yaml.RNode) ([]*yaml.RNode, error) { 299 // AndSelector or OrSelector doesn't really matter here since there is only one matcher (m). 300 s := AndSelector{Matchers: []ResourceMatcher{m}, TemplateData: m.TemplateData} 301 return s.Filter(items) 302 } 303 304 var _ ResourceTemplateMatcher = &TemplatedMetaMapMatcher{} 305 306 func templatizeSlice(values []string, data interface{}) ([]string, error) { 307 if data == nil { 308 return values, nil 309 } 310 var err error 311 results := make([]string, len(values)) 312 for i := range values { 313 results[i], err = templatize(values[i], data) 314 if err != nil { 315 return nil, errors.WrapPrefixf(err, "unable to render template %s", values[i]) 316 } 317 } 318 return results, nil 319 } 320 321 func templatizeMap(values map[string]string, data interface{}) (map[string]string, error) { 322 if data == nil { 323 return values, nil 324 } 325 var err error 326 results := make(map[string]string, len(values)) 327 328 for k := range values { 329 results[k], err = templatize(values[k], data) 330 if err != nil { 331 return nil, errors.WrapPrefixf(err, "unable to render template for %s=%s", k, values[k]) 332 } 333 } 334 return results, nil 335 } 336 337 // templatize renders the value as a template, using the provided data 338 func templatize(value string, data interface{}) (string, error) { 339 t, err := template.New("kinds").Parse(value) 340 if err != nil { 341 return "", errors.Wrap(err) 342 } 343 var b bytes.Buffer 344 err = t.Execute(&b, data) 345 if err != nil { 346 return "", errors.Wrap(err) 347 } 348 return b.String(), nil 349 } 350