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  }