istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pilot/pkg/config/file/store.go (about) 1 /* 2 Copyright Istio Authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package file 18 19 import ( 20 "bufio" 21 "bytes" 22 "crypto/sha256" 23 "encoding/json" 24 "errors" 25 "fmt" 26 "io" 27 "strings" 28 "sync" 29 30 "github.com/hashicorp/go-multierror" 31 yamlv3 "gopkg.in/yaml.v3" 32 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 34 "k8s.io/apimachinery/pkg/runtime" 35 "k8s.io/apimachinery/pkg/runtime/serializer" 36 kubeJson "k8s.io/apimachinery/pkg/runtime/serializer/json" 37 "k8s.io/apimachinery/pkg/util/yaml" 38 39 kubeyaml2 "istio.io/istio/pilot/pkg/config/file/util/kubeyaml" 40 "istio.io/istio/pilot/pkg/config/memory" 41 "istio.io/istio/pilot/pkg/model" 42 "istio.io/istio/pkg/config" 43 legacykube "istio.io/istio/pkg/config/analysis/legacy/source/kube" 44 "istio.io/istio/pkg/config/resource" 45 "istio.io/istio/pkg/config/schema/collection" 46 sresource "istio.io/istio/pkg/config/schema/resource" 47 "istio.io/istio/pkg/kube" 48 "istio.io/istio/pkg/log" 49 "istio.io/istio/pkg/slices" 50 "istio.io/istio/pkg/util/sets" 51 ) 52 53 var ( 54 inMemoryKubeNameDiscriminator int64 55 scope = log.RegisterScope("file", "File client messages") 56 ) 57 58 // KubeSource is an in-memory source implementation that can handle K8s style resources. 59 type KubeSource struct { 60 mu sync.Mutex 61 62 name string 63 schemas *collection.Schemas 64 inner model.ConfigStore 65 defaultNs resource.Namespace 66 67 shas map[kubeResourceKey]resourceSha 68 byFile map[string]map[kubeResourceKey]config.GroupVersionKind 69 70 // If meshConfig.DiscoverySelectors are specified, the namespacesFilter tracks the namespaces this controller watches. 71 namespacesFilter func(obj interface{}) bool 72 } 73 74 func (s *KubeSource) Schemas() collection.Schemas { 75 return *s.schemas 76 } 77 78 func (s *KubeSource) Get(typ config.GroupVersionKind, name, namespace string) *config.Config { 79 return s.inner.Get(typ, name, namespace) 80 } 81 82 func (s *KubeSource) List(typ config.GroupVersionKind, namespace string) []config.Config { 83 configs := s.inner.List(typ, namespace) 84 if s.namespacesFilter != nil { 85 return slices.Filter(configs, func(c config.Config) bool { 86 return s.namespacesFilter(c) 87 }) 88 } 89 return configs 90 } 91 92 func (s *KubeSource) Create(config config.Config) (revision string, err error) { 93 return s.inner.Create(config) 94 } 95 96 func (s *KubeSource) Update(config config.Config) (newRevision string, err error) { 97 return s.inner.Update(config) 98 } 99 100 func (s *KubeSource) UpdateStatus(config config.Config) (newRevision string, err error) { 101 return s.inner.UpdateStatus(config) 102 } 103 104 func (s *KubeSource) Patch(orig config.Config, patchFn config.PatchFunc) (string, error) { 105 return s.inner.Patch(orig, patchFn) 106 } 107 108 func (s *KubeSource) Delete(typ config.GroupVersionKind, name, namespace string, resourceVersion *string) error { 109 return s.inner.Delete(typ, name, namespace, resourceVersion) 110 } 111 112 func (s *KubeSource) RegisterEventHandler(kind config.GroupVersionKind, handler model.EventHandler) { 113 panic("implement me") 114 } 115 116 func (s *KubeSource) Run(stop <-chan struct{}) { 117 } 118 119 func (s *KubeSource) HasSynced() bool { 120 return true 121 } 122 123 type resourceSha [sha256.Size]byte 124 125 type kubeResource struct { 126 // resource *resource.Instance 127 config *config.Config 128 schema sresource.Schema 129 sha resourceSha 130 } 131 132 func (r *kubeResource) newKey() kubeResourceKey { 133 return kubeResourceKey{ 134 kind: r.schema.Kind(), 135 fullName: r.fullName(), 136 } 137 } 138 139 func (r *kubeResource) fullName() resource.FullName { 140 return resource.NewFullName(resource.Namespace(r.config.Namespace), 141 resource.LocalName(r.config.Name)) 142 } 143 144 type kubeResourceKey struct { 145 fullName resource.FullName 146 kind string 147 } 148 149 var _ model.ConfigStore = &KubeSource{} 150 151 // NewKubeSource returns a new in-memory Source that works with Kubernetes resources. 152 func NewKubeSource(schemas collection.Schemas) *KubeSource { 153 name := fmt.Sprintf("kube-inmemory-%d", inMemoryKubeNameDiscriminator) 154 inMemoryKubeNameDiscriminator++ 155 156 return &KubeSource{ 157 name: name, 158 schemas: &schemas, 159 inner: memory.MakeSkipValidation(schemas), 160 shas: make(map[kubeResourceKey]resourceSha), 161 byFile: make(map[string]map[kubeResourceKey]config.GroupVersionKind), 162 } 163 } 164 165 // SetDefaultNamespace enables injecting a default namespace for resources where none is already specified 166 func (s *KubeSource) SetDefaultNamespace(defaultNs resource.Namespace) { 167 s.defaultNs = defaultNs 168 } 169 170 // SetNamespacesFilter enables filtering the namespaces this controller watches. 171 func (s *KubeSource) SetNamespacesFilter(namespacesFilter func(obj interface{}) bool) { 172 s.namespacesFilter = namespacesFilter 173 } 174 175 // Clear the contents of this source 176 func (s *KubeSource) Clear() { 177 s.shas = make(map[kubeResourceKey]resourceSha) 178 s.byFile = make(map[string]map[kubeResourceKey]config.GroupVersionKind) 179 s.inner = memory.MakeSkipValidation(*s.schemas) 180 } 181 182 // ContentNames returns the names known to this source. 183 func (s *KubeSource) ContentNames() map[string]struct{} { 184 s.mu.Lock() 185 defer s.mu.Unlock() 186 187 result := sets.New[string]() 188 for n := range s.byFile { 189 result.Insert(n) 190 } 191 192 return result 193 } 194 195 // ApplyContent applies the given yamltext to this source. The content is tracked with the given name. If ApplyContent 196 // gets called multiple times with the same name, the contents applied by the previous incarnation will be overwritten 197 // or removed, depending on the new content. 198 // Returns an error if any were encountered, but that still may represent a partial success 199 func (s *KubeSource) ApplyContent(name, yamlText string) error { 200 s.mu.Lock() 201 defer s.mu.Unlock() 202 203 // We hold off on dealing with parseErr until the end, since partial success is possible 204 resources, parseErrs := s.parseContent(s.schemas, name, yamlText) 205 206 oldKeys := s.byFile[name] 207 newKeys := make(map[kubeResourceKey]config.GroupVersionKind) 208 209 for _, r := range resources { 210 key := r.newKey() 211 212 oldSha, found := s.shas[key] 213 if !found || oldSha != r.sha { 214 scope.Debugf("KubeSource.ApplyContent: Set: %v/%v", r.schema.GroupVersionKind(), r.fullName()) 215 // apply is idempotent, but configstore is not, thus the odd logic here 216 _, err := s.inner.Update(*r.config) 217 if err != nil { 218 _, err = s.inner.Create(*r.config) 219 if err != nil { 220 return fmt.Errorf("cannot store config %s/%s %s from reader: %s", 221 r.schema.Version(), r.schema.Kind(), r.fullName(), err) 222 } 223 } 224 s.shas[key] = r.sha 225 } 226 newKeys[key] = r.schema.GroupVersionKind() 227 if oldKeys != nil { 228 scope.Debugf("KubeSource.ApplyContent: Delete: %v/%v", r.schema.GroupVersionKind(), key) 229 delete(oldKeys, key) 230 } 231 } 232 233 for k, col := range oldKeys { 234 empty := "" 235 err := s.inner.Delete(col, k.fullName.Name.String(), k.fullName.Namespace.String(), &empty) 236 if err != nil { 237 scope.Errorf("encountered unexpected error removing resource from filestore: %s", err) 238 } 239 } 240 s.byFile[name] = newKeys 241 242 if parseErrs != nil { 243 return fmt.Errorf("errors parsing content %q: %v", name, parseErrs) 244 } 245 return nil 246 } 247 248 // RemoveContent removes the content for the given name 249 func (s *KubeSource) RemoveContent(name string) { 250 s.mu.Lock() 251 defer s.mu.Unlock() 252 253 keys := s.byFile[name] 254 if keys != nil { 255 for key, col := range keys { 256 empty := "" 257 err := s.inner.Delete(col, key.fullName.Name.String(), key.fullName.Namespace.String(), &empty) 258 if err != nil { 259 scope.Errorf("encountered unexpected error removing resource from filestore: %s", err) 260 } 261 delete(s.shas, key) 262 } 263 264 delete(s.byFile, name) 265 } 266 } 267 268 func (s *KubeSource) parseContent(r *collection.Schemas, name, yamlText string) ([]kubeResource, error) { 269 var resources []kubeResource 270 var errs error 271 272 reader := bufio.NewReader(strings.NewReader(yamlText)) 273 decoder := kubeyaml2.NewYAMLReader(reader) 274 chunkCount := -1 275 276 for { 277 chunkCount++ 278 doc, lineNum, err := decoder.Read() 279 if err == io.EOF { 280 break 281 } 282 if err != nil { 283 e := fmt.Errorf("error reading documents in %s[%d]: %v", name, chunkCount, err) 284 scope.Warnf("%v - skipping", e) 285 scope.Debugf("Failed to parse yamlText chunk: %v", yamlText) 286 errs = multierror.Append(errs, e) 287 break 288 } 289 290 chunk := bytes.TrimSpace(doc) 291 if len(chunk) == 0 { 292 continue 293 } 294 chunkResources, err := s.parseChunk(r, name, lineNum, chunk) 295 if err != nil { 296 var uerr *unknownSchemaError 297 if errors.As(err, &uerr) { 298 scope.Debugf("skipping unknown yaml chunk %s: %s", name, uerr.Error()) 299 } else { 300 e := fmt.Errorf("error processing %s[%d]: %v", name, chunkCount, err) 301 scope.Warnf("%v - skipping", e) 302 scope.Debugf("Failed to parse yaml chunk: %v", string(chunk)) 303 errs = multierror.Append(errs, e) 304 } 305 continue 306 } 307 resources = append(resources, chunkResources...) 308 } 309 310 return resources, errs 311 } 312 313 // unknownSchemaError represents a schema was not found for a group+version+kind. 314 type unknownSchemaError struct { 315 group string 316 version string 317 kind string 318 } 319 320 func (e unknownSchemaError) Error() string { 321 return fmt.Sprintf("failed finding schema for group/version/kind: %s/%s/%s", e.group, e.version, e.kind) 322 } 323 324 func (s *KubeSource) parseChunk(r *collection.Schemas, name string, lineNum int, yamlChunk []byte) ([]kubeResource, error) { 325 resources := make([]kubeResource, 0) 326 // Convert to JSON 327 jsonChunk, err := yaml.ToJSON(yamlChunk) 328 if err != nil { 329 return resources, fmt.Errorf("failed converting YAML to JSON: %v", err) 330 } 331 332 // ignore null json 333 if len(jsonChunk) == 0 || bytes.Equal(jsonChunk, []byte("null")) { 334 return resources, nil 335 } 336 337 // Peek at the beginning of the JSON to 338 groupVersionKind, err := kubeJson.DefaultMetaFactory.Interpret(jsonChunk) 339 if err != nil { 340 return resources, fmt.Errorf("failed interpreting jsonChunk: %v", err) 341 } 342 343 if groupVersionKind.Kind == "List" { 344 resourceChunks, err := extractResourceChunksFromListYamlChunk(yamlChunk) 345 if err != nil { 346 return resources, fmt.Errorf("failed extracting resource chunks from list yaml chunk: %v", err) 347 } 348 for _, resourceChunk := range resourceChunks { 349 lr, err := s.parseChunk(r, name, resourceChunk.lineNum+lineNum, resourceChunk.yamlChunk) 350 if err != nil { 351 return resources, fmt.Errorf("failed parsing resource chunk: %v", err) 352 } 353 resources = append(resources, lr...) 354 } 355 return resources, nil 356 } 357 358 if groupVersionKind.Empty() { 359 return resources, fmt.Errorf("unable to parse resource with no group, version and kind") 360 } 361 362 schema, found := r.FindByGroupVersionAliasesKind(sresource.FromKubernetesGVK(groupVersionKind)) 363 364 if !found { 365 return resources, &unknownSchemaError{ 366 group: groupVersionKind.Group, 367 version: groupVersionKind.Version, 368 kind: groupVersionKind.Kind, 369 } 370 } 371 372 // Cannot create new instance. This occurs because while newer types do not implement proto.Message, 373 // this legacy code only supports proto.Messages. 374 // Note: while NewInstance can be slightly modified to not return error here, the rest of the code 375 // still requires a proto.Message so it won't work without completely refactoring galley/ 376 _, e := schema.NewInstance() 377 cannotHandleProto := e != nil 378 if cannotHandleProto { 379 return resources, &unknownSchemaError{ 380 group: groupVersionKind.Group, 381 version: groupVersionKind.Version, 382 kind: groupVersionKind.Kind, 383 } 384 } 385 386 runtimeScheme := runtime.NewScheme() 387 codecs := serializer.NewCodecFactory(runtimeScheme) 388 deserializer := codecs.UniversalDeserializer() 389 obj, err := kube.IstioScheme.New(schema.GroupVersionKind().Kubernetes()) 390 if err != nil { 391 return resources, fmt.Errorf("failed to initialize interface for built-in type: %v", err) 392 } 393 _, _, err = deserializer.Decode(jsonChunk, nil, obj) 394 if err != nil { 395 return resources, fmt.Errorf("failed parsing JSON for built-in type: %v", err) 396 } 397 objMeta, ok := obj.(metav1.Object) 398 if !ok { 399 return resources, errors.New("failed to assert type of object metadata") 400 } 401 402 // If namespace is blank and we have a default set, fill in the default 403 // (This mirrors the behavior if you kubectl apply a resource without a namespace defined) 404 // Don't do this for cluster scoped resources 405 if !schema.IsClusterScoped() { 406 if objMeta.GetNamespace() == "" && s.defaultNs != "" { 407 scope.Debugf("KubeSource.parseChunk: namespace not specified for %q, using %q", objMeta.GetName(), s.defaultNs) 408 objMeta.SetNamespace(string(s.defaultNs)) 409 } 410 } else { 411 // Clear the namespace if there is any specified. 412 objMeta.SetNamespace("") 413 } 414 415 // Build flat map for analyzers if the line JSON object exists, if the YAML text is ill-formed, this will be nil 416 fieldMap := make(map[string]int) 417 418 // yamlv3.Node contains information like line number of the node, which will be used with its name to construct the field map 419 yamlChunkNode := yamlv3.Node{} 420 err = yamlv3.Unmarshal(yamlChunk, &yamlChunkNode) 421 if err == nil && len(yamlChunkNode.Content) == 1 { 422 423 // Get the Node that contains all the YAML chunk information 424 yamlNode := yamlChunkNode.Content[0] 425 426 BuildFieldPathMap(yamlNode, lineNum, "", fieldMap) 427 } 428 429 pos := legacykube.Position{Filename: name, Line: lineNum} 430 c, err := ToConfig(objMeta, schema, &pos, fieldMap) 431 if err != nil { 432 return resources, err 433 } 434 return []kubeResource{ 435 { 436 schema: schema, 437 sha: sha256.Sum256(yamlChunk), 438 config: c, 439 }, 440 }, nil 441 } 442 443 type resourceYamlChunk struct { 444 lineNum int 445 yamlChunk []byte 446 } 447 448 func extractResourceChunksFromListYamlChunk(chunk []byte) ([]resourceYamlChunk, error) { 449 chunks := make([]resourceYamlChunk, 0) 450 yamlChunkNode := yamlv3.Node{} 451 err := yamlv3.Unmarshal(chunk, &yamlChunkNode) 452 if err != nil { 453 return nil, fmt.Errorf("failed parsing yamlChunk: %v", err) 454 } 455 if len(yamlChunkNode.Content) == 0 { 456 return nil, fmt.Errorf("failed parsing yamlChunk: no content") 457 } 458 yamlNode := yamlChunkNode.Content[0] 459 var itemsInd int 460 for ; itemsInd < len(yamlNode.Content); itemsInd++ { 461 if yamlNode.Content[itemsInd].Kind == yamlv3.ScalarNode && yamlNode.Content[itemsInd].Value == "items" { 462 itemsInd++ 463 break 464 } 465 } 466 if itemsInd >= len(yamlNode.Content) || yamlNode.Content[itemsInd].Kind != yamlv3.SequenceNode { 467 return nil, fmt.Errorf("failed parsing yamlChunk: malformed items field") 468 } 469 for _, n := range yamlNode.Content[itemsInd].Content { 470 if n.Kind != yamlv3.MappingNode { 471 return nil, fmt.Errorf("failed parsing yamlChunk: malformed items field") 472 } 473 resourceChunk, err := yamlv3.Marshal(n) 474 if err != nil { 475 return nil, fmt.Errorf("failed marshaling yamlChunk: %v", err) 476 } 477 chunks = append(chunks, resourceYamlChunk{ 478 lineNum: n.Line, 479 yamlChunk: resourceChunk, 480 }) 481 } 482 return chunks, nil 483 } 484 485 const ( 486 FieldMapKey = "istiofilefieldmap" 487 ReferenceKey = "istiosource" 488 ) 489 490 // ToConfig converts the given object and proto to a config.Config 491 func ToConfig(object metav1.Object, schema sresource.Schema, source resource.Reference, fieldMap map[string]int) (*config.Config, error) { 492 m, err := runtime.DefaultUnstructuredConverter.ToUnstructured(object) 493 if err != nil { 494 return nil, err 495 } 496 u := &unstructured.Unstructured{Object: m} 497 if len(fieldMap) > 0 || source != nil { 498 // TODO: populate 499 annots := u.GetAnnotations() 500 if annots == nil { 501 annots = map[string]string{} 502 } 503 jsonfm, err := json.Marshal(fieldMap) 504 if err != nil { 505 return nil, err 506 } 507 annots[FieldMapKey] = string(jsonfm) 508 jsonsource, err := json.Marshal(source) 509 if err != nil { 510 return nil, err 511 } 512 annots[ReferenceKey] = string(jsonsource) 513 u.SetAnnotations(annots) 514 } 515 result := TranslateObject(u, "", schema) 516 return result, nil 517 } 518 519 func TranslateObject(obj *unstructured.Unstructured, domainSuffix string, schema sresource.Schema) *config.Config { 520 mv2, err := schema.NewInstance() 521 if err != nil { 522 panic(err) 523 } 524 if spec, ok := obj.UnstructuredContent()["spec"]; ok { 525 err = runtime.DefaultUnstructuredConverter.FromUnstructured(spec.(map[string]any), mv2) 526 } else { 527 err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), mv2) 528 } 529 if err != nil { 530 panic(err) 531 } 532 533 m := obj 534 return &config.Config{ 535 Meta: config.Meta{ 536 GroupVersionKind: schema.GroupVersionKind(), 537 UID: string(m.GetUID()), 538 Name: m.GetName(), 539 Namespace: m.GetNamespace(), 540 Labels: m.GetLabels(), 541 Annotations: m.GetAnnotations(), 542 ResourceVersion: m.GetResourceVersion(), 543 CreationTimestamp: m.GetCreationTimestamp().Time, 544 OwnerReferences: m.GetOwnerReferences(), 545 Generation: m.GetGeneration(), 546 Domain: domainSuffix, 547 }, 548 Spec: mv2, 549 } 550 } 551 552 // BuildFieldPathMap builds the flat map for each field of the YAML resource 553 func BuildFieldPathMap(yamlNode *yamlv3.Node, startLineNum int, curPath string, fieldPathMap map[string]int) { 554 // If no content in the node, terminate the DFS search 555 if len(yamlNode.Content) == 0 { 556 return 557 } 558 559 nodeContent := yamlNode.Content 560 // Iterate content by a step of 2, because in the content array the value is in the key's next index position 561 for i := 0; i < len(nodeContent)-1; i += 2 { 562 // Two condition, i + 1 positions have no content, which means they have the format like "key: value", then build the map 563 // Or i + 1 has contents, which means "key:\n value...", then perform one more DFS search 564 keyNode := nodeContent[i] 565 valueNode := nodeContent[i+1] 566 pathKeyForMap := fmt.Sprintf("%s.%s", curPath, keyNode.Value) 567 568 switch { 569 case valueNode.Kind == yamlv3.ScalarNode: 570 // Can build map because the value node has no content anymore 571 // minus one because startLineNum starts at line 1, and yamlv3.Node.line also starts at line 1 572 fieldPathMap[fmt.Sprintf("{%s}", pathKeyForMap)] = valueNode.Line + startLineNum - 1 573 574 case valueNode.Kind == yamlv3.MappingNode: 575 BuildFieldPathMap(valueNode, startLineNum, pathKeyForMap, fieldPathMap) 576 577 case valueNode.Kind == yamlv3.SequenceNode: 578 for j, node := range valueNode.Content { 579 pathWithIndex := fmt.Sprintf("%s[%d]", pathKeyForMap, j) 580 581 // Array with values or array with maps 582 if node.Kind == yamlv3.ScalarNode { 583 fieldPathMap[fmt.Sprintf("{%s}", pathWithIndex)] = node.Line + startLineNum - 1 584 } else { 585 BuildFieldPathMap(node, startLineNum, pathWithIndex, fieldPathMap) 586 } 587 } 588 } 589 } 590 }