github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/yamltags/tags.go (about) 1 /* 2 Copyright 2019 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 yamltags 18 19 import ( 20 "context" 21 "fmt" 22 "reflect" 23 "strings" 24 "unicode" 25 26 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log" 27 "github.com/GoogleContainerTools/skaffold/pkg/skaffold/yaml" 28 ) 29 30 type fieldSet map[string]struct{} 31 32 // ValidateStruct validates and processes the provided pointer to a struct. 33 func ValidateStruct(s interface{}) error { 34 parentStruct := reflect.Indirect(reflect.ValueOf(s)) 35 t := parentStruct.Type() 36 log.Entry(context.TODO()).Tracef("validating yamltags of struct %s", t.Name()) 37 38 // Loop through the fields on the struct, looking for tags. 39 for i := 0; i < t.NumField(); i++ { 40 f := t.Field(i) 41 val := parentStruct.Field(i) 42 field := parentStruct.Type().Field(i) 43 if tags, ok := f.Tag.Lookup("yamltags"); ok { 44 if err := processTags(tags, val, parentStruct, field); err != nil { 45 return err 46 } 47 } 48 } 49 return nil 50 } 51 52 // YamlName returns the YAML name of the given field 53 func YamlName(field reflect.StructField) string { 54 if yamltags, ok := field.Tag.Lookup("yaml"); ok { 55 tags := strings.Split(yamltags, ",") 56 if len(tags) > 0 && tags[0] != "" { 57 return tags[0] 58 } 59 } 60 return field.Name 61 } 62 63 // GetYamlTag returns the first yaml tag used in the raw yaml text of the given struct 64 func GetYamlTag(value interface{}) string { 65 buf, err := yaml.Marshal(value) 66 if err != nil { 67 log.Entry(context.TODO()).Warnf("error marshaling %-v", value) 68 return "" 69 } 70 rawStr := string(buf) 71 i := strings.Index(rawStr, ":") 72 if i == -1 { 73 return "" 74 } 75 return rawStr[:i] 76 } 77 78 // GetYamlKeys returns the yaml key for each non-nested field of the given non-nil config parameter 79 // For example if config is `latest.DeployType{HelmDeploy: &HelmDeploy{...}, KustomizeDeploy: &KustomizeDeploy{...}}` 80 // then it returns `["helm", "kustomize"]` 81 func GetYamlKeys(config interface{}) []string { 82 var tags []string 83 if config == nil { 84 return tags 85 } 86 st := reflect.Indirect(reflect.ValueOf(config)) 87 t := st.Type() 88 for i := 0; i < t.NumField(); i++ { 89 f := t.Field(i) 90 v := st.Field(i) 91 if v.Kind() == reflect.Ptr && v.IsNil() { // exclude ptr fields not explicitly defined in the configuration 92 continue 93 } 94 tag := getYamlKey(f) 95 if tag != "" { 96 tags = append(tags, tag) 97 } 98 } 99 return tags 100 } 101 102 func getYamlKey(f reflect.StructField) string { 103 if f.PkgPath != "" { // field is unexported 104 return "" 105 } 106 t, ok := f.Tag.Lookup("yaml") 107 if !ok { 108 return lowerCaseFirst(f.Name) 109 } 110 tags := strings.Split(t, ",") 111 for i := 1; i < len(tags); i++ { // return empty string if it contains yaml flag `inline` 112 if tags[i] == "inline" { 113 return "" 114 } 115 } 116 if len(tags) == 0 || tags[0] == "" { 117 return lowerCaseFirst(f.Name) 118 } 119 return tags[0] 120 } 121 122 func lowerCaseFirst(s string) string { 123 r := []rune(s) 124 r[0] = unicode.ToLower(r[0]) 125 return string(r) 126 } 127 128 func processTags(yamltags string, val reflect.Value, parentStruct reflect.Value, field reflect.StructField) error { 129 tags := strings.Split(yamltags, ",") 130 for _, tag := range tags { 131 tagParts := strings.Split(tag, "=") 132 var yt yamlTag 133 switch tagParts[0] { 134 case "required": 135 yt = &requiredTag{ 136 Field: field, 137 } 138 case "oneOf": 139 yt = &oneOfTag{ 140 Field: field, 141 Parent: parentStruct, 142 } 143 case "skipTrim": 144 yt = &skipTrimTag{ 145 Field: field, 146 } 147 default: 148 log.Entry(context.TODO()).Panicf("unknown yaml tag in %s", yamltags) 149 } 150 if err := yt.Load(tagParts); err != nil { 151 return err 152 } 153 if err := yt.Process(val); err != nil { 154 return err 155 } 156 } 157 return nil 158 } 159 160 type yamlTag interface { 161 Load([]string) error 162 Process(reflect.Value) error 163 } 164 165 type requiredTag struct { 166 Field reflect.StructField 167 } 168 169 func (rt *requiredTag) Load(s []string) error { 170 return nil 171 } 172 173 func (rt *requiredTag) Process(val reflect.Value) error { 174 if isZeroValue(val) { 175 if tags, ok := rt.Field.Tag.Lookup("yaml"); ok { 176 return fmt.Errorf("required value not set: %s", strings.Split(tags, ",")[0]) 177 } 178 return fmt.Errorf("required value not set: %s", rt.Field.Name) 179 } 180 return nil 181 } 182 183 // A program can have many structs, that each have many oneOfSets. 184 // Each oneOfSet is a map of a oneOf-set name to the set of fields that belong to that oneOf-set 185 // Only one field in that set may have a non-zero value. 186 187 var allOneOfs map[string]map[string]fieldSet 188 189 func getOneOfSetsForStruct(structName string) map[string]fieldSet { 190 _, ok := allOneOfs[structName] 191 if !ok { 192 allOneOfs[structName] = map[string]fieldSet{} 193 } 194 return allOneOfs[structName] 195 } 196 197 type oneOfTag struct { 198 Field reflect.StructField 199 Parent reflect.Value 200 oneOfSets map[string]fieldSet 201 setName string 202 } 203 204 func (oot *oneOfTag) Load(s []string) error { 205 if len(s) != 2 { 206 return fmt.Errorf("invalid default struct tag: %v, expected key=value", s) 207 } 208 oot.setName = s[1] 209 210 // Fetch the right oneOfSet for the struct. 211 structName := oot.Parent.Type().Name() 212 oot.oneOfSets = getOneOfSetsForStruct(structName) 213 214 // Add this field to the oneOfSet 215 if _, ok := oot.oneOfSets[oot.setName]; !ok { 216 oot.oneOfSets[oot.setName] = fieldSet{} 217 } 218 oot.oneOfSets[oot.setName][oot.Field.Name] = struct{}{} 219 return nil 220 } 221 222 func (oot *oneOfTag) Process(val reflect.Value) error { 223 if isZeroValue(val) { 224 return nil 225 } 226 227 // This must exist because process is always called after Load. 228 oneOfSet := oot.oneOfSets[oot.setName] 229 for otherField := range oneOfSet { 230 if otherField == oot.Field.Name { 231 continue 232 } 233 field := oot.Parent.FieldByName(otherField) 234 if !isZeroValue(field) { 235 return fmt.Errorf("only one element in set %s can be set. got %s and %s", oot.setName, otherField, oot.Field.Name) 236 } 237 } 238 return nil 239 } 240 241 type skipTrimTag struct { 242 Field reflect.StructField 243 } 244 245 func (tag *skipTrimTag) Load(s []string) error { 246 return nil 247 } 248 249 func (tag *skipTrimTag) Process(val reflect.Value) error { 250 if isZeroValue(val) { 251 if tags, ok := tag.Field.Tag.Lookup("yaml"); ok { 252 return fmt.Errorf("skipTrim value not set: %s", strings.Split(tags, ",")[0]) 253 } 254 return fmt.Errorf("skipTrim value not set: %s", tag.Field.Name) 255 } 256 return nil 257 } 258 259 func isZeroValue(val reflect.Value) bool { 260 if val.Kind() == reflect.Invalid { 261 return true 262 } 263 zv := reflect.Zero(val.Type()).Interface() 264 return reflect.DeepEqual(zv, val.Interface()) 265 } 266 267 func init() { 268 allOneOfs = make(map[string]map[string]fieldSet) 269 }