k8s.io/apiserver@v0.31.1/pkg/endpoints/openapi/openapi.go (about) 1 /* 2 Copyright 2016 The Kubernetes 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 openapi 18 19 import ( 20 "bytes" 21 "fmt" 22 "reflect" 23 "sort" 24 "strings" 25 "unicode" 26 27 restful "github.com/emicklei/go-restful/v3" 28 29 v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 30 "k8s.io/apimachinery/pkg/runtime" 31 "k8s.io/apimachinery/pkg/runtime/schema" 32 "k8s.io/kube-openapi/pkg/util" 33 "k8s.io/kube-openapi/pkg/validation/spec" 34 ) 35 36 var verbs = util.NewTrie([]string{"get", "log", "read", "replace", "patch", "delete", "deletecollection", "watch", "connect", "proxy", "list", "create", "patch"}) 37 38 const ( 39 extensionGVK = "x-kubernetes-group-version-kind" 40 ) 41 42 // ToValidOperationID makes an string a valid op ID (e.g. removing punctuations and whitespaces and make it camel case) 43 func ToValidOperationID(s string, capitalizeFirstLetter bool) string { 44 var buffer bytes.Buffer 45 capitalize := capitalizeFirstLetter 46 for i, r := range s { 47 if unicode.IsLetter(r) || r == '_' || (i != 0 && unicode.IsDigit(r)) { 48 if capitalize { 49 buffer.WriteRune(unicode.ToUpper(r)) 50 capitalize = false 51 } else { 52 buffer.WriteRune(r) 53 } 54 } else { 55 capitalize = true 56 } 57 } 58 return buffer.String() 59 } 60 61 // GetOperationIDAndTags returns a customize operation ID and a list of tags for kubernetes API server's OpenAPI spec to prevent duplicate IDs. 62 func GetOperationIDAndTags(r *restful.Route) (string, []string, error) { 63 op := r.Operation 64 path := r.Path 65 var tags []string 66 prefix, exists := verbs.GetPrefix(op) 67 if !exists { 68 return op, tags, fmt.Errorf("operation names should start with a verb. Cannot determine operation verb from %v", op) 69 } 70 op = op[len(prefix):] 71 parts := strings.Split(strings.Trim(path, "/"), "/") 72 // Assume /api is /apis/core, remove this when we actually server /api/... on /apis/core/... 73 if len(parts) >= 1 && parts[0] == "api" { 74 parts = append([]string{"apis", "core"}, parts[1:]...) 75 } 76 if len(parts) >= 2 && parts[0] == "apis" { 77 trimmed := strings.TrimSuffix(parts[1], ".k8s.io") 78 prefix = prefix + ToValidOperationID(trimmed, prefix != "") 79 tag := ToValidOperationID(trimmed, false) 80 if len(parts) > 2 { 81 prefix = prefix + ToValidOperationID(parts[2], prefix != "") 82 tag = tag + "_" + ToValidOperationID(parts[2], false) 83 } 84 tags = append(tags, tag) 85 } else if len(parts) >= 1 { 86 tags = append(tags, ToValidOperationID(parts[0], false)) 87 } 88 return prefix + ToValidOperationID(op, prefix != ""), tags, nil 89 } 90 91 type groupVersionKinds []v1.GroupVersionKind 92 93 func (s groupVersionKinds) Len() int { 94 return len(s) 95 } 96 97 func (s groupVersionKinds) Swap(i, j int) { 98 s[i], s[j] = s[j], s[i] 99 } 100 101 func (s groupVersionKinds) Less(i, j int) bool { 102 if s[i].Group == s[j].Group { 103 if s[i].Version == s[j].Version { 104 return s[i].Kind < s[j].Kind 105 } 106 return s[i].Version < s[j].Version 107 } 108 return s[i].Group < s[j].Group 109 } 110 111 func (s groupVersionKinds) JSON() []interface{} { 112 j := []interface{}{} 113 for _, gvk := range s { 114 j = append(j, map[string]interface{}{ 115 "group": gvk.Group, 116 "version": gvk.Version, 117 "kind": gvk.Kind, 118 }) 119 } 120 return j 121 } 122 123 // DefinitionNamer is the type to customize OpenAPI definition name. 124 type DefinitionNamer struct { 125 typeGroupVersionKinds map[string]groupVersionKinds 126 } 127 128 func gvkConvert(gvk schema.GroupVersionKind) v1.GroupVersionKind { 129 return v1.GroupVersionKind{ 130 Group: gvk.Group, 131 Version: gvk.Version, 132 Kind: gvk.Kind, 133 } 134 } 135 136 func friendlyName(name string) string { 137 nameParts := strings.Split(name, "/") 138 // Reverse first part. e.g., io.k8s... instead of k8s.io... 139 if len(nameParts) > 0 && strings.Contains(nameParts[0], ".") { 140 parts := strings.Split(nameParts[0], ".") 141 for i, j := 0, len(parts)-1; i < j; i, j = i+1, j-1 { 142 parts[i], parts[j] = parts[j], parts[i] 143 } 144 nameParts[0] = strings.Join(parts, ".") 145 } 146 return strings.Join(nameParts, ".") 147 } 148 149 func typeName(t reflect.Type) string { 150 path := t.PkgPath() 151 if strings.Contains(path, "/vendor/") { 152 path = path[strings.Index(path, "/vendor/")+len("/vendor/"):] 153 } 154 return fmt.Sprintf("%s.%s", path, t.Name()) 155 } 156 157 // NewDefinitionNamer constructs a new DefinitionNamer to be used to customize OpenAPI spec. 158 func NewDefinitionNamer(schemes ...*runtime.Scheme) *DefinitionNamer { 159 ret := &DefinitionNamer{ 160 typeGroupVersionKinds: map[string]groupVersionKinds{}, 161 } 162 for _, s := range schemes { 163 for gvk, rtype := range s.AllKnownTypes() { 164 newGVK := gvkConvert(gvk) 165 exists := false 166 for _, existingGVK := range ret.typeGroupVersionKinds[typeName(rtype)] { 167 if newGVK == existingGVK { 168 exists = true 169 break 170 } 171 } 172 if !exists { 173 ret.typeGroupVersionKinds[typeName(rtype)] = append(ret.typeGroupVersionKinds[typeName(rtype)], newGVK) 174 } 175 } 176 } 177 for _, gvk := range ret.typeGroupVersionKinds { 178 sort.Sort(gvk) 179 } 180 return ret 181 } 182 183 // GetDefinitionName returns the name and tags for a given definition 184 func (d *DefinitionNamer) GetDefinitionName(name string) (string, spec.Extensions) { 185 if groupVersionKinds, ok := d.typeGroupVersionKinds[name]; ok { 186 return friendlyName(name), spec.Extensions{ 187 extensionGVK: groupVersionKinds.JSON(), 188 } 189 } 190 return friendlyName(name), nil 191 }