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 }