1/* 2Copyright 2018 The Kubernetes Authors. 3 4Licensed under the Apache License, Version 2.0 (the "License"); 5you may not use this file except in compliance with the License. 6You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10Unless required by applicable law or agreed to in writing, software 11distributed under the License is distributed on an "AS IS" BASIS, 12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13See the License for the specific language governing permissions and 14limitations under the License. 15*/ 16 17package generators 18 19import ( 20 "fmt" 21 "sort" 22 "strings" 23 24 "k8s.io/gengo/examples/set-gen/sets" 25 "k8s.io/gengo/types" 26) 27 28const extensionPrefix = "x-kubernetes-" 29 30// extensionAttributes encapsulates common traits for particular extensions. 31type extensionAttributes struct { 32 xName string 33 kind types.Kind 34 allowedValues sets.String 35 enforceArray bool 36} 37 38// Extension tag to openapi extension attributes 39var tagToExtension = map[string]extensionAttributes{ 40 "patchMergeKey": { 41 xName: "x-kubernetes-patch-merge-key", 42 kind: types.Slice, 43 }, 44 "patchStrategy": { 45 xName: "x-kubernetes-patch-strategy", 46 kind: types.Slice, 47 allowedValues: sets.NewString("merge", "retainKeys"), 48 }, 49 "listMapKey": { 50 xName: "x-kubernetes-list-map-keys", 51 kind: types.Slice, 52 enforceArray: true, 53 }, 54 "listType": { 55 xName: "x-kubernetes-list-type", 56 kind: types.Slice, 57 allowedValues: sets.NewString("atomic", "set", "map"), 58 }, 59} 60 61// Extension encapsulates information necessary to generate an OpenAPI extension. 62type extension struct { 63 idlTag string // Example: listType 64 xName string // Example: x-kubernetes-list-type 65 values []string // Example: [atomic] 66} 67 68func (e extension) hasAllowedValues() bool { 69 return tagToExtension[e.idlTag].allowedValues.Len() > 0 70} 71 72func (e extension) allowedValues() sets.String { 73 return tagToExtension[e.idlTag].allowedValues 74} 75 76func (e extension) hasKind() bool { 77 return len(tagToExtension[e.idlTag].kind) > 0 78} 79 80func (e extension) kind() types.Kind { 81 return tagToExtension[e.idlTag].kind 82} 83 84func (e extension) validateAllowedValues() error { 85 // allowedValues not set means no restrictions on values. 86 if !e.hasAllowedValues() { 87 return nil 88 } 89 // Check for missing value. 90 if len(e.values) == 0 { 91 return fmt.Errorf("%s needs a value, none given.", e.idlTag) 92 } 93 // For each extension value, validate that it is allowed. 94 allowedValues := e.allowedValues() 95 if !allowedValues.HasAll(e.values...) { 96 return fmt.Errorf("%v not allowed for %s. Allowed values: %v", 97 e.values, e.idlTag, allowedValues.List()) 98 } 99 return nil 100} 101 102func (e extension) validateType(kind types.Kind) error { 103 // If this extension class has no kind, then don't validate the type. 104 if !e.hasKind() { 105 return nil 106 } 107 if kind != e.kind() { 108 return fmt.Errorf("tag %s on type %v; only allowed on type %v", 109 e.idlTag, kind, e.kind()) 110 } 111 return nil 112} 113 114func (e extension) hasMultipleValues() bool { 115 return len(e.values) > 1 116} 117 118func (e extension) isAlwaysArrayFormat() bool { 119 return tagToExtension[e.idlTag].enforceArray 120} 121 122// Returns sorted list of map keys. Needed for deterministic testing. 123func sortedMapKeys(m map[string][]string) []string { 124 keys := make([]string, len(m)) 125 i := 0 126 for k := range m { 127 keys[i] = k 128 i++ 129 } 130 sort.Strings(keys) 131 return keys 132} 133 134// Parses comments to return openapi extensions. Returns a list of 135// extensions which parsed correctly, as well as a list of the 136// parse errors. Validating extensions is performed separately. 137// NOTE: Non-empty errors does not mean extensions is empty. 138func parseExtensions(comments []string) ([]extension, []error) { 139 extensions := []extension{} 140 errors := []error{} 141 // First, generate extensions from "+k8s:openapi-gen=x-kubernetes-*" annotations. 142 values := getOpenAPITagValue(comments) 143 for _, val := range values { 144 // Example: x-kubernetes-member-tag:member_test 145 if strings.HasPrefix(val, extensionPrefix) { 146 parts := strings.SplitN(val, ":", 2) 147 if len(parts) != 2 { 148 errors = append(errors, fmt.Errorf("invalid extension value: %v", val)) 149 continue 150 } 151 e := extension{ 152 idlTag: tagName, // Example: k8s:openapi-gen 153 xName: parts[0], // Example: x-kubernetes-member-tag 154 values: []string{parts[1]}, // Example: member_test 155 } 156 extensions = append(extensions, e) 157 } 158 } 159 // Next, generate extensions from "idlTags" (e.g. +listType) 160 tagValues := types.ExtractCommentTags("+", comments) 161 for _, idlTag := range sortedMapKeys(tagValues) { 162 xAttrs, exists := tagToExtension[idlTag] 163 if !exists { 164 continue 165 } 166 values := tagValues[idlTag] 167 e := extension{ 168 idlTag: idlTag, // listType 169 xName: xAttrs.xName, // x-kubernetes-list-type 170 values: values, // [atomic] 171 } 172 extensions = append(extensions, e) 173 } 174 return extensions, errors 175} 176 177func validateMemberExtensions(extensions []extension, m *types.Member) []error { 178 errors := []error{} 179 for _, e := range extensions { 180 if err := e.validateAllowedValues(); err != nil { 181 errors = append(errors, err) 182 } 183 if err := e.validateType(m.Type.Kind); err != nil { 184 errors = append(errors, err) 185 } 186 } 187 return errors 188} 189