github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/parser/configlocations/configlocations.go (about)

     1  /*
     2  Copyright 2021 The Skaffold 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 configlocations
    18  
    19  import (
    20  	"context"
    21  	"path"
    22  	"reflect"
    23  	"strconv"
    24  	"strings"
    25  
    26  	kyaml "sigs.k8s.io/kustomize/kyaml/yaml"
    27  
    28  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
    29  	sErrors "github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/errors"
    30  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    31  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
    32  )
    33  
    34  type YAMLInfo struct {
    35  	RNode      *kyaml.RNode
    36  	SourceFile string
    37  }
    38  
    39  type Location struct {
    40  	SourceFile  string
    41  	StartLine   int
    42  	StartColumn int
    43  	EndLine     int
    44  	EndColumn   int
    45  }
    46  
    47  type YAMLInfos struct {
    48  	yamlInfos               map[uintptr]map[string]YAMLInfo
    49  	FieldsOverrodeByProfile map[string]YAMLOverrideInfo // map of schema path -> profile name -- ex: /artifacts/0/image -> "overwrite-artifacte-image-profile"
    50  }
    51  
    52  func (m *YAMLInfos) GetYamlInfosCopy() map[uintptr]map[string]YAMLInfo {
    53  	yamlInfos := map[uintptr]map[string]YAMLInfo{}
    54  	for ptr, mp := range m.yamlInfos {
    55  		tmpmp := map[string]YAMLInfo{}
    56  		for k, v := range mp {
    57  			tmpmp[k] = YAMLInfo{
    58  				RNode:      v.RNode.Copy(),
    59  				SourceFile: v.SourceFile,
    60  			}
    61  		}
    62  		yamlInfos[ptr] = tmpmp
    63  	}
    64  	return yamlInfos
    65  }
    66  
    67  func MissingLocation() *Location {
    68  	return &Location{
    69  		SourceFile:  "",
    70  		StartLine:   -1,
    71  		StartColumn: -1,
    72  		EndLine:     -1,
    73  		EndColumn:   -1,
    74  	}
    75  }
    76  
    77  func NewYAMLInfos() *YAMLInfos {
    78  	return &YAMLInfos{
    79  		yamlInfos: map[uintptr]map[string]YAMLInfo{},
    80  	}
    81  }
    82  
    83  type YAMLOverrideInfo struct {
    84  	ProfileName    string
    85  	PatchIndex     int
    86  	PatchOperation string
    87  	PatchCopyFrom  string
    88  }
    89  
    90  // Parse parses a skaffold config entry collecting file location information for each schema config object
    91  func Parse(sourceFile string, config *latest.SkaffoldConfig, fieldsOverrodeByProfile map[string]YAMLOverrideInfo) (*YAMLInfos, error) {
    92  	yamlInfos, err := buildMapOfSchemaObjPointerToYAMLInfos(sourceFile, config, map[uintptr]map[string]YAMLInfo{}, fieldsOverrodeByProfile)
    93  	return &YAMLInfos{
    94  			yamlInfos:               yamlInfos,
    95  			FieldsOverrodeByProfile: fieldsOverrodeByProfile,
    96  		},
    97  		err
    98  }
    99  
   100  // Locate gets the location for a skaffold schema struct pointer
   101  func (m *YAMLInfos) Locate(obj interface{}) *Location {
   102  	return m.locate(obj, "")
   103  }
   104  
   105  // Locate gets the location for a skaffold schema struct pointer
   106  func (m *YAMLInfos) LocateElement(obj interface{}, idx int) *Location {
   107  	return m.locate(obj, strconv.Itoa(idx))
   108  }
   109  
   110  // Locate gets the location for a skaffold schema struct pointer
   111  func (m *YAMLInfos) LocateField(obj interface{}, fieldName string) *Location {
   112  	return m.locate(obj, fieldName)
   113  }
   114  
   115  // Locate gets the location for a skaffold schema struct pointer
   116  func (m *YAMLInfos) LocateByPointer(ptr uintptr) *Location {
   117  	if m == nil {
   118  		log.Entry(context.TODO()).Infof("YamlInfos is nil, unable to complete call to LocateByPointer for pointer: %d", ptr)
   119  		return MissingLocation()
   120  	}
   121  	if _, ok := m.yamlInfos[ptr]; !ok {
   122  		log.Entry(context.TODO()).Infof("no map entry found when attempting LocateByPointer for pointer: %d", ptr)
   123  		return MissingLocation()
   124  	}
   125  	node, ok := m.yamlInfos[ptr][""]
   126  	if !ok {
   127  		log.Entry(context.TODO()).Infof("no map entry found when attempting LocateByPointer for pointer: %d", ptr)
   128  		return MissingLocation()
   129  	}
   130  	// iterate over kyaml.RNode text to get endline and endcolumn information
   131  	nodeText, err := node.RNode.String()
   132  	if err != nil {
   133  		return MissingLocation()
   134  	}
   135  	log.Entry(context.TODO()).Infof("map entry found when executing LocateByPointer for pointer: %d", ptr)
   136  	lines, cols := getLinesAndColsOfString(nodeText)
   137  
   138  	// TODO(aaron-prindle) all line & col values seem 1 greater than expected in actual use, will need to check to see how it works with IDE
   139  	return &Location{
   140  		SourceFile:  node.SourceFile,
   141  		StartLine:   node.RNode.Document().Line,
   142  		StartColumn: node.RNode.Document().Column,
   143  		EndLine:     node.RNode.Document().Line + lines,
   144  		EndColumn:   cols,
   145  	}
   146  }
   147  
   148  func (m *YAMLInfos) locate(obj interface{}, key string) *Location {
   149  	if m == nil {
   150  		log.Entry(context.TODO()).Infof("YamlInfos is nil, unable to complete call to locate with params: %v of type %T", obj, obj)
   151  		return MissingLocation()
   152  	}
   153  	v := reflect.ValueOf(obj)
   154  	if v.Kind() != reflect.Ptr {
   155  		log.Entry(context.TODO()).Infof("non pointer object passed to locate: %v of type %T", obj, obj)
   156  		return MissingLocation()
   157  	}
   158  	if _, ok := m.yamlInfos[v.Pointer()]; !ok {
   159  		log.Entry(context.TODO()).Infof("no map entry found when attempting locate for %v of type %T and pointer: %d", obj, obj, v.Pointer())
   160  		return MissingLocation()
   161  	}
   162  	node, ok := m.yamlInfos[v.Pointer()][key]
   163  	if !ok {
   164  		log.Entry(context.TODO()).Infof("no map entry found when attempting locate for %v of type %T and pointer: %d", obj, obj, v.Pointer())
   165  		return MissingLocation()
   166  	}
   167  	// iterate over kyaml.RNode text to get endline and endcolumn information
   168  	nodeText, err := node.RNode.String()
   169  	if err != nil {
   170  		return MissingLocation()
   171  	}
   172  	log.Entry(context.TODO()).Infof("map entry found when executing locate for %v of type %T and pointer: %d", obj, obj, v.Pointer())
   173  	lines, cols := getLinesAndColsOfString(nodeText)
   174  
   175  	// TODO(aaron-prindle) all line & col values seem 1 greater than expected in actual use, will need to check to see how it works with IDE
   176  	return &Location{
   177  		SourceFile:  node.SourceFile,
   178  		StartLine:   node.RNode.Document().Line,
   179  		StartColumn: node.RNode.Document().Column,
   180  		EndLine:     node.RNode.Document().Line + lines,
   181  		EndColumn:   cols,
   182  	}
   183  }
   184  
   185  func getLinesAndColsOfString(str string) (int, int) {
   186  	line := 0
   187  	col := 0
   188  	for i := range str {
   189  		col++
   190  		if str[i] == '\n' {
   191  			line++
   192  			col = 0
   193  		}
   194  	}
   195  	return line, col
   196  }
   197  
   198  func buildMapOfSchemaObjPointerToYAMLInfos(sourceFile string, config *latest.SkaffoldConfig, yamlInfos map[uintptr]map[string]YAMLInfo,
   199  	fieldsOverrodeByProfile map[string]YAMLOverrideInfo) (map[uintptr]map[string]YAMLInfo, error) {
   200  	defer func() {
   201  		if err := recover(); err != nil {
   202  			log.Entry(context.TODO()).Errorf(
   203  				"panic occurred during schema reflection for yaml line number information: %v", err)
   204  		}
   205  	}()
   206  
   207  	skaffoldConfigText, err := util.ReadConfiguration(sourceFile)
   208  	if err != nil {
   209  		return nil, sErrors.ConfigParsingError(err)
   210  	}
   211  	root, err := kyaml.Parse(string(skaffoldConfigText))
   212  	if err != nil {
   213  		return nil, err
   214  	}
   215  
   216  	return generateObjPointerToYAMLNodeMap(sourceFile, reflect.ValueOf(config), reflect.ValueOf(nil), "", "", []string{},
   217  		root, root, -1, fieldsOverrodeByProfile, yamlInfos, false)
   218  }
   219  
   220  // generateObjPointerToYAMLNodeMap recursively walks through a structs fields (taking into account profile and patch profile overrides)
   221  // and collects the corresponding yaml node for each field
   222  func generateObjPointerToYAMLNodeMap(sourceFile string, v reflect.Value, parentV reflect.Value, fieldName, yamlTag string, schemaPath []string,
   223  	rootRNode *kyaml.RNode, rNode *kyaml.RNode, containerIdx int, fieldPathsOverrodeByProfiles map[string]YAMLOverrideInfo, yamlInfos map[uintptr]map[string]YAMLInfo, isPatchProfileElemOverride bool) (map[uintptr]map[string]YAMLInfo, error) {
   224  	// TODO(aaron-prindle) need to verify if generateObjPointerToYAMLNodeMap adds entries for 'map' types, luckily the skaffold schema
   225  	// only has map[string]string and they are leaf nodes as well which this should work fine for doing the recursion for the time being
   226  	var err error
   227  
   228  	// add current obj/field to schema path if criteria met
   229  	switch {
   230  	case containerIdx >= 0:
   231  		schemaPath = append(schemaPath, strconv.Itoa(containerIdx))
   232  	case yamlTag != "":
   233  		schemaPath = append(schemaPath, yamlTag)
   234  	}
   235  	// check if current obj/field was overridden by a profile
   236  	if yamlOverrideInfo, ok := fieldPathsOverrodeByProfiles["/"+path.Join(schemaPath...)]; ok {
   237  		// reset yaml node path from root path to given profile path ("/" -> "/profile/name=profileName/etc...")
   238  		rNode, err = rootRNode.Pipe(kyaml.Lookup("profiles"), kyaml.MatchElementList([]string{"name"}, []string{yamlOverrideInfo.ProfileName}))
   239  		if err != nil {
   240  			return nil, err
   241  		}
   242  		switch {
   243  		case yamlOverrideInfo.PatchIndex < 0: // this schema obj/field has a profile override (NOT a patch profile override)
   244  			// moves parent node path from being rooted at default yaml '/' to being rooted at '/profile/name=profileName/...'
   245  			for i := 0; i < len(schemaPath)-1; i++ {
   246  				rNode, err = rNode.Pipe(kyaml.Lookup(schemaPath[i]))
   247  				if err != nil {
   248  					return nil, err
   249  				}
   250  			}
   251  		default: // this schema obj/field has a patch profile override
   252  			// NOTE: 'remove' patch operations are not included in fieldPathsOverrodeByProfiles as there
   253  			//  is no work to be done on them (they were already removed from the schema)
   254  
   255  			// TODO(aaron-prindle) verify UX makes sense to use the "FROM" copy node to get yaml information from
   256  			if yamlOverrideInfo.PatchOperation == "copy" {
   257  				fromPath := strings.Split(yamlOverrideInfo.PatchCopyFrom, "/")
   258  				var kf kyaml.Filter
   259  				for i := 0; i < len(fromPath)-1; i++ {
   260  					if pathNum, err := strconv.Atoi(fromPath[i]); err == nil {
   261  						// this path element is a number
   262  						kf = kyaml.ElementIndexer{Index: pathNum}
   263  					} else {
   264  						// this path element isn't a number
   265  						kf = kyaml.Lookup(fromPath[i])
   266  					}
   267  					rNode, err = rNode.Pipe(kf)
   268  					if err != nil {
   269  						return nil, err
   270  					}
   271  				}
   272  			} else {
   273  				rNode, err = rNode.Pipe(kyaml.Lookup("patches"), kyaml.ElementIndexer{Index: yamlOverrideInfo.PatchIndex})
   274  				if err != nil {
   275  					return nil, err
   276  				}
   277  				yamlTag = "value"
   278  			}
   279  			isPatchProfileElemOverride = true
   280  		}
   281  	}
   282  	if rNode == nil {
   283  		return yamlInfos, nil
   284  	}
   285  
   286  	// drill down through pointers and interfaces to get a value we can use
   287  	for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
   288  		v = v.Elem()
   289  	}
   290  
   291  	if yamlTag != "" { // check that struct is not `yaml:",inline"`
   292  		// traverse kyaml node tree to current obj/field location
   293  		var kf kyaml.Filter
   294  		switch {
   295  		case rNode.YNode().Kind == kyaml.SequenceNode:
   296  			kf = kyaml.ElementIndexer{Index: containerIdx}
   297  		default:
   298  			kf = kyaml.Lookup(yamlTag)
   299  		}
   300  		rNode, err = rNode.Pipe(kf)
   301  		if err != nil {
   302  			return nil, err
   303  		}
   304  		if rNode == nil {
   305  			return yamlInfos, nil
   306  		}
   307  
   308  		// this case is so that the line #'s of primitive values can be "located" as they are not addressable but we can
   309  		// map the parent address and put the child field in second map
   310  		if parentV.CanAddr() {
   311  			if _, ok := yamlInfos[parentV.Addr().Pointer()]; !ok {
   312  				yamlInfos[parentV.Addr().Pointer()] = map[string]YAMLInfo{}
   313  			}
   314  			// add parent relationship entry to yaml info map
   315  			if containerIdx >= 0 {
   316  				yamlInfos[parentV.Addr().Pointer()][strconv.Itoa(containerIdx)] = YAMLInfo{
   317  					RNode:      rNode,
   318  					SourceFile: sourceFile,
   319  				}
   320  			} else {
   321  				yamlInfos[parentV.Addr().Pointer()][fieldName] = YAMLInfo{
   322  					RNode:      rNode,
   323  					SourceFile: sourceFile,
   324  				}
   325  			}
   326  		}
   327  	}
   328  
   329  	if v.CanAddr() {
   330  		if _, ok := yamlInfos[v.Addr().Pointer()]; !ok {
   331  			yamlInfos[v.Addr().Pointer()] = map[string]YAMLInfo{}
   332  		}
   333  		// add current node entry to yaml info map
   334  		yamlInfos[v.Addr().Pointer()][""] = YAMLInfo{
   335  			RNode:      rNode,
   336  			SourceFile: sourceFile,
   337  		}
   338  	}
   339  
   340  	switch v.Kind() {
   341  	// TODO(aaron-prindle) add reflect.Map support here as well, currently no struct fields have nested struct in map field so ok for now
   342  	case reflect.Slice, reflect.Array:
   343  		for i := 0; i < v.Len(); i++ {
   344  			generateObjPointerToYAMLNodeMap(sourceFile, v.Index(i), v, fieldName+"["+strconv.Itoa(i)+"]", yamlTag+"["+strconv.Itoa(i)+"]", schemaPath,
   345  				rootRNode, rNode, i, fieldPathsOverrodeByProfiles, yamlInfos, isPatchProfileElemOverride)
   346  		}
   347  	case reflect.Struct:
   348  		t := v.Type() // use type to get number and names of fields
   349  		for i := 0; i < t.NumField(); i++ {
   350  			field := t.Field(i)
   351  			// TODO(aaron-prindle) verify this value works for structs that are `yaml:",inline"`
   352  			newYamlTag := field.Name
   353  			if yamlTagToken := field.Tag.Get("yaml"); yamlTagToken != "" && yamlTagToken != "-" {
   354  				// check for possible comma as in "...,omitempty"
   355  				var commaIdx int
   356  				if commaIdx = strings.Index(yamlTagToken, ","); commaIdx < 0 {
   357  					commaIdx = len(yamlTagToken)
   358  				}
   359  				newYamlTag = yamlTagToken[:commaIdx]
   360  			}
   361  			generateObjPointerToYAMLNodeMap(sourceFile, v.Field(i), v, field.Name, newYamlTag, schemaPath, rootRNode, rNode, -1,
   362  				fieldPathsOverrodeByProfiles, yamlInfos, isPatchProfileElemOverride)
   363  		}
   364  	}
   365  	return yamlInfos, nil
   366  }