blob: e512f29b38c9d2f18f7c4026cf05f8d7afc0dc08 [file] [log] [blame]
Matthias Andreas Benkard832a54e2019-01-29 09:27:38 +01001/*
2Copyright 2016 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 openapi
18
19import (
20 "bytes"
21 "fmt"
22 "reflect"
23 "sort"
24 "strings"
25 "unicode"
26
27 restful "github.com/emicklei/go-restful"
28 "github.com/go-openapi/spec"
29
30 "k8s.io/apimachinery/pkg/apis/meta/v1"
31 "k8s.io/apimachinery/pkg/runtime"
32 "k8s.io/apimachinery/pkg/runtime/schema"
33 "k8s.io/kube-openapi/pkg/util"
34)
35
36var verbs = util.NewTrie([]string{"get", "log", "read", "replace", "patch", "delete", "deletecollection", "watch", "connect", "proxy", "list", "create", "patch"})
37
38const (
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)
43func 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.
62func 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
91type groupVersionKinds []v1.GroupVersionKind
92
93func (s groupVersionKinds) Len() int {
94 return len(s)
95}
96
97func (s groupVersionKinds) Swap(i, j int) {
98 s[i], s[j] = s[j], s[i]
99}
100
101func (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// DefinitionNamer is the type to customize OpenAPI definition name.
112type DefinitionNamer struct {
113 typeGroupVersionKinds map[string]groupVersionKinds
114}
115
116func gvkConvert(gvk schema.GroupVersionKind) v1.GroupVersionKind {
117 return v1.GroupVersionKind{
118 Group: gvk.Group,
119 Version: gvk.Version,
120 Kind: gvk.Kind,
121 }
122}
123
124func friendlyName(name string) string {
125 nameParts := strings.Split(name, "/")
126 // Reverse first part. e.g., io.k8s... instead of k8s.io...
127 if len(nameParts) > 0 && strings.Contains(nameParts[0], ".") {
128 parts := strings.Split(nameParts[0], ".")
129 for i, j := 0, len(parts)-1; i < j; i, j = i+1, j-1 {
130 parts[i], parts[j] = parts[j], parts[i]
131 }
132 nameParts[0] = strings.Join(parts, ".")
133 }
134 return strings.Join(nameParts, ".")
135}
136
137func typeName(t reflect.Type) string {
138 path := t.PkgPath()
139 if strings.Contains(path, "/vendor/") {
140 path = path[strings.Index(path, "/vendor/")+len("/vendor/"):]
141 }
142 return fmt.Sprintf("%s.%s", path, t.Name())
143}
144
145// NewDefinitionNamer constructs a new DefinitionNamer to be used to customize OpenAPI spec.
146func NewDefinitionNamer(schemes ...*runtime.Scheme) *DefinitionNamer {
147 ret := &DefinitionNamer{
148 typeGroupVersionKinds: map[string]groupVersionKinds{},
149 }
150 for _, s := range schemes {
151 for gvk, rtype := range s.AllKnownTypes() {
152 newGVK := gvkConvert(gvk)
153 exists := false
154 for _, existingGVK := range ret.typeGroupVersionKinds[typeName(rtype)] {
155 if newGVK == existingGVK {
156 exists = true
157 break
158 }
159 }
160 if !exists {
161 ret.typeGroupVersionKinds[typeName(rtype)] = append(ret.typeGroupVersionKinds[typeName(rtype)], newGVK)
162 }
163 }
164 }
165 for _, gvk := range ret.typeGroupVersionKinds {
166 sort.Sort(gvk)
167 }
168 return ret
169}
170
171// GetDefinitionName returns the name and tags for a given definition
172func (d *DefinitionNamer) GetDefinitionName(name string) (string, spec.Extensions) {
173 if groupVersionKinds, ok := d.typeGroupVersionKinds[name]; ok {
174 return friendlyName(name), spec.Extensions{
175 extensionGVK: []v1.GroupVersionKind(groupVersionKinds),
176 }
177 }
178 return friendlyName(name), nil
179}