2 Copyright 2018 The Kubernetes Authors.
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
8 http://www.apache.org/licenses/LICENSE-2.0
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.
26 "github.com/pkg/errors"
27 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
32 specReplicasPath = "specpath"
33 statusReplicasPath = "statuspath"
34 labelSelectorPath = "selectorpath"
35 jsonPathError = "invalid scale path. specpath, statuspath key-value pairs are required, only selectorpath key-value is optinal. For example: // +kubebuilder:subresource:scale:specpath=.spec.replica,statuspath=.status.replica,selectorpath=.spec.Label"
36 printColumnName = "name"
37 printColumnType = "type"
38 printColumnDescr = "description"
39 printColumnPath = "JSONPath"
40 printColumnFormat = "format"
41 printColumnPri = "priority"
42 printColumnError = "invalid printcolumn path. name,type, and JSONPath are required kye-value pairs and rest of the fields are optinal. For example: // +kubebuilder:printcolumn:name=abc,type=string,JSONPath=status"
45 // Options contains the parser options
47 SkipMapValidation bool
49 // SkipRBACValidation flag determines whether to check RBAC annotations
50 // for the controller or not at parse stage.
51 SkipRBACValidation bool
54 // IsAPIResource returns true if either of the two conditions become true:
55 // 1. t has a +resource/+kubebuilder:resource comment tag
56 // 2. t has TypeMeta and ObjectMeta in its member list.
57 func IsAPIResource(t *types.Type) bool {
58 for _, c := range t.CommentLines {
59 if strings.Contains(c, "+resource") || strings.Contains(c, "+kubebuilder:resource") {
64 typeMetaFound, objMetaFound := false, false
65 for _, m := range t.Members {
66 if m.Name == "TypeMeta" && m.Type.String() == "k8s.io/apimachinery/pkg/apis/meta/v1.TypeMeta" {
69 if m.Name == "ObjectMeta" && m.Type.String() == "k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta" {
72 if typeMetaFound && objMetaFound {
79 // IsNonNamespaced returns true if t has a +nonNamespaced comment tag
80 func IsNonNamespaced(t *types.Type) bool {
81 if !IsAPIResource(t) {
85 for _, c := range t.CommentLines {
86 if strings.Contains(c, "+genclient:nonNamespaced") {
91 for _, c := range t.SecondClosestCommentLines {
92 if strings.Contains(c, "+genclient:nonNamespaced") {
100 // IsController returns true if t has a +controller or +kubebuilder:controller tag
101 func IsController(t *types.Type) bool {
102 for _, c := range t.CommentLines {
103 if strings.Contains(c, "+controller") || strings.Contains(c, "+kubebuilder:controller") {
110 // IsRBAC returns true if t has a +rbac or +kubebuilder:rbac tag
111 func IsRBAC(t *types.Type) bool {
112 for _, c := range t.CommentLines {
113 if strings.Contains(c, "+rbac") || strings.Contains(c, "+kubebuilder:rbac") {
120 // hasPrintColumn returns true if t has a +printcolumn or +kubebuilder:printcolumn annotation.
121 func hasPrintColumn(t *types.Type) bool {
122 for _, c := range t.CommentLines {
123 if strings.Contains(c, "+printcolumn") || strings.Contains(c, "+kubebuilder:printcolumn") {
130 // IsInformer returns true if t has a +informers or +kubebuilder:informers tag
131 func IsInformer(t *types.Type) bool {
132 for _, c := range t.CommentLines {
133 if strings.Contains(c, "+informers") || strings.Contains(c, "+kubebuilder:informers") {
140 // IsAPISubresource returns true if t has a +subresource-request comment tag
141 func IsAPISubresource(t *types.Type) bool {
142 for _, c := range t.CommentLines {
143 if strings.Contains(c, "+subresource-request") {
150 // HasSubresource returns true if t is an APIResource with one or more Subresources
151 func HasSubresource(t *types.Type) bool {
152 if !IsAPIResource(t) {
155 for _, c := range t.CommentLines {
156 if strings.Contains(c, "subresource") {
163 // hasStatusSubresource returns true if t is an APIResource annotated with
164 // +kubebuilder:subresource:status
165 func hasStatusSubresource(t *types.Type) bool {
166 if !IsAPIResource(t) {
169 for _, c := range t.CommentLines {
170 if strings.Contains(c, "+kubebuilder:subresource:status") {
177 // hasScaleSubresource returns true if t is an APIResource annotated with
178 // +kubebuilder:subresource:scale
179 func hasScaleSubresource(t *types.Type) bool {
180 if !IsAPIResource(t) {
183 for _, c := range t.CommentLines {
184 if strings.Contains(c, "+kubebuilder:subresource:scale") {
191 // hasCategories returns true if t is an APIResource annotated with
192 // +kubebuilder:categories
193 func hasCategories(t *types.Type) bool {
194 if !IsAPIResource(t) {
198 for _, c := range t.CommentLines {
199 if strings.Contains(c, "+kubebuilder:categories") {
206 // HasDocAnnotation returns true if t is an APIResource with doc annotation
208 func HasDocAnnotation(t *types.Type) bool {
209 if !IsAPIResource(t) {
212 for _, c := range t.CommentLines {
213 if strings.Contains(c, "+kubebuilder:doc") {
220 // hasSingular returns true if t is an APIResource annotated with
221 // +kubebuilder:singular
222 func hasSingular(t *types.Type) bool {
223 if !IsAPIResource(t) {
226 for _, c := range t.CommentLines{
227 if strings.Contains(c, "+kubebuilder:singular"){
234 // IsUnversioned returns true if t is in given group, and not in versioned path.
235 func IsUnversioned(t *types.Type, group string) bool {
236 return IsApisDir(filepath.Base(filepath.Dir(t.Name.Package))) && GetGroup(t) == group
239 // IsVersioned returns true if t is in given group, and in versioned path.
240 func IsVersioned(t *types.Type, group string) bool {
241 dir := filepath.Base(filepath.Dir(filepath.Dir(t.Name.Package)))
242 return IsApisDir(dir) && GetGroup(t) == group
245 // GetVersion returns version of t.
246 func GetVersion(t *types.Type, group string) string {
247 if !IsVersioned(t, group) {
248 panic(errors.Errorf("Cannot get version for unversioned type %v", t.Name))
250 return filepath.Base(t.Name.Package)
253 // GetGroup returns group of t.
254 func GetGroup(t *types.Type) string {
255 return filepath.Base(GetGroupPackage(t))
258 // GetGroupPackage returns group package of t.
259 func GetGroupPackage(t *types.Type) string {
260 if IsApisDir(filepath.Base(filepath.Dir(t.Name.Package))) {
261 return t.Name.Package
263 return filepath.Dir(t.Name.Package)
266 // GetKind returns kind of t.
267 func GetKind(t *types.Type, group string) string {
268 if !IsVersioned(t, group) && !IsUnversioned(t, group) {
269 panic(errors.Errorf("Cannot get kind for type not in group %v", t.Name))
274 // IsApisDir returns true if a directory path is a Kubernetes api directory
275 func IsApisDir(dir string) bool {
276 return dir == "apis" || dir == "api"
279 // Comments is a structure for using comment tags on go structs and fields
280 type Comments []string
282 // GetTags returns the value for the first comment with a prefix matching "+name="
283 // e.g. "+name=foo\n+name=bar" would return "foo"
284 func (c Comments) getTag(name, sep string) string {
285 for _, c := range c {
286 prefix := fmt.Sprintf("+%s%s", name, sep)
287 if strings.HasPrefix(c, prefix) {
288 return strings.Replace(c, prefix, "", 1)
294 // hasTag returns true if the Comments has a tag with the given name
295 func (c Comments) hasTag(name string) bool {
296 for _, c := range c {
297 prefix := fmt.Sprintf("+%s", name)
298 if strings.HasPrefix(c, prefix) {
305 // GetTags returns the value for all comments with a prefix and separator. E.g. for "name" and "="
306 // "+name=foo\n+name=bar" would return []string{"foo", "bar"}
307 func (c Comments) getTags(name, sep string) []string {
309 for _, c := range c {
310 prefix := fmt.Sprintf("+%s%s", name, sep)
311 if strings.HasPrefix(c, prefix) {
312 tags = append(tags, strings.Replace(c, prefix, "", 1))
318 // getCategoriesTag returns the value of the +kubebuilder:categories tags
319 func getCategoriesTag(c *types.Type) string {
320 comments := Comments(c.CommentLines)
321 resource := comments.getTag("kubebuilder:categories", "=")
322 if len(resource) == 0 {
323 panic(errors.Errorf("Must specify +kubebuilder:categories comment for type %v", c.Name))
328 // getSingularName returns the value of the +kubebuilder:singular tag
329 func getSingularName(c *types.Type) string {
330 comments := Comments(c.CommentLines)
331 singular := comments.getTag("kubebuilder:singular", "=")
332 if len(singular) == 0 {
333 panic(errors.Errorf("Must specify a value to use with +kubebuilder:singular comment for type %v", c.Name))
338 // getDocAnnotation parse annotations of "+kubebuilder:doc:" with tags of "warning" or "doc" for control generating doc config.
339 // E.g. +kubebuilder:doc:warning=foo +kubebuilder:doc:note=bar
340 func getDocAnnotation(t *types.Type, tags ...string) map[string]string {
341 annotation := make(map[string]string)
342 for _, tag := range tags {
343 for _, c := range t.CommentLines {
344 prefix := fmt.Sprintf("+kubebuilder:doc:%s=", tag)
345 if strings.HasPrefix(c, prefix) {
346 annotation[tag] = strings.Replace(c, prefix, "", 1)
353 // parseByteValue returns the literal digital number values from a byte array
354 func parseByteValue(b []byte) string {
355 elem := strings.Join(strings.Fields(fmt.Sprintln(b)), ",")
356 elem = strings.TrimPrefix(elem, "[")
357 elem = strings.TrimSuffix(elem, "]")
361 // parseDescription parse comments above each field in the type definition.
362 func parseDescription(res []string) string {
363 var temp strings.Builder
365 for _, comment := range res {
366 if !(strings.Contains(comment, "+kubebuilder") || strings.Contains(comment, "+optional")) {
367 temp.WriteString(comment)
368 temp.WriteString(" ")
369 desc = strings.TrimRight(temp.String(), " ")
375 // parseEnumToString returns a representive validated go format string from JSONSchemaProps schema
376 func parseEnumToString(value []v1beta1.JSON) string {
377 res := "[]v1beta1.JSON{"
378 prefix := "v1beta1.JSON{[]byte{"
379 for _, v := range value {
380 res = res + prefix + parseByteValue(v.Raw) + "}},"
382 return strings.TrimSuffix(res, ",") + "}"
385 // check type of enum element value to match type of field
386 func checkType(props *v1beta1.JSONSchemaProps, s string, enums *[]v1beta1.JSON) {
388 // TODO support more types check
390 case "int", "int64", "uint64":
391 if _, err := strconv.ParseInt(s, 0, 64); err != nil {
392 log.Fatalf("Invalid integer value [%v] for a field of integer type", s)
394 *enums = append(*enums, v1beta1.JSON{Raw: []byte(fmt.Sprintf("%v", s))})
395 case "int32", "unit32":
396 if _, err := strconv.ParseInt(s, 0, 32); err != nil {
397 log.Fatalf("Invalid integer value [%v] for a field of integer32 type", s)
399 *enums = append(*enums, v1beta1.JSON{Raw: []byte(fmt.Sprintf("%v", s))})
400 case "float", "float32":
401 if _, err := strconv.ParseFloat(s, 32); err != nil {
402 log.Fatalf("Invalid float value [%v] for a field of float32 type", s)
404 *enums = append(*enums, v1beta1.JSON{Raw: []byte(fmt.Sprintf("%v", s))})
406 if _, err := strconv.ParseFloat(s, 64); err != nil {
407 log.Fatalf("Invalid float value [%v] for a field of float type", s)
409 *enums = append(*enums, v1beta1.JSON{Raw: []byte(fmt.Sprintf("%v", s))})
411 *enums = append(*enums, v1beta1.JSON{Raw: []byte(`"` + s + `"`)})
415 // Scale subresource requires specpath, statuspath, selectorpath key values, represents for JSONPath of
416 // SpecReplicasPath, StatusReplicasPath, LabelSelectorPath separately. e.g.
417 // +kubebuilder:subresource:scale:specpath=.spec.replica,statuspath=.status.replica,selectorpath=
418 func parseScaleParams(t *types.Type) (map[string]string, error) {
419 jsonPath := make(map[string]string)
420 for _, c := range t.CommentLines {
421 if strings.Contains(c, "+kubebuilder:subresource:scale") {
422 paths := strings.Replace(c, "+kubebuilder:subresource:scale:", "", -1)
423 path := strings.Split(paths, ",")
425 return nil, fmt.Errorf(jsonPathError)
427 for _, s := range path {
428 kv := strings.Split(s, "=")
429 if kv[0] == specReplicasPath || kv[0] == statusReplicasPath || kv[0] == labelSelectorPath {
430 jsonPath[kv[0]] = kv[1]
432 return nil, fmt.Errorf(jsonPathError)
436 _, ok = jsonPath[specReplicasPath]
438 return nil, fmt.Errorf(jsonPathError)
440 _, ok = jsonPath[statusReplicasPath]
442 return nil, fmt.Errorf(jsonPathError)
447 return nil, fmt.Errorf(jsonPathError)
450 // printColumnKV parses key-value string formatted as "foo=bar" and returns key and value.
451 func printColumnKV(s string) (key, value string, err error) {
452 kv := strings.SplitN(s, "=", 2)
454 err = fmt.Errorf("invalid key value pair")
455 return key, value, err
457 key, value = kv[0], kv[1]
458 if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
459 value = value[1 : len(value)-1]
461 return key, value, err
464 // helperPrintColumn is a helper function for the parsePrintColumnParams to compute printer columns.
465 func helperPrintColumn(parts string, comment string) (v1beta1.CustomResourceColumnDefinition, error) {
466 config := v1beta1.CustomResourceColumnDefinition{}
468 part := strings.Split(parts, ",")
470 return v1beta1.CustomResourceColumnDefinition{}, fmt.Errorf(printColumnError)
473 for _, elem := range strings.Split(parts, ",") {
474 key, value, err := printColumnKV(elem)
476 return v1beta1.CustomResourceColumnDefinition{},
477 fmt.Errorf("//+kubebuilder:printcolumn: tags must be key value pairs.Expected "+
478 "keys [name=<name>,type=<type>,description=<descr>,format=<format>] "+
479 "Got string: [%s]", parts)
481 if key == printColumnName || key == printColumnType || key == printColumnPath {
485 case printColumnName:
487 case printColumnType:
488 if value == "integer" || value == "number" || value == "string" || value == "boolean" || value == "date" {
491 return v1beta1.CustomResourceColumnDefinition{}, fmt.Errorf("invalid value for %s printcolumn", printColumnType)
493 case printColumnFormat:
494 if config.Type == "integer" && (value == "int32" || value == "int64") {
495 config.Format = value
496 } else if config.Type == "number" && (value == "float" || value == "double") {
497 config.Format = value
498 } else if config.Type == "string" && (value == "byte" || value == "date" || value == "date-time" || value == "password") {
499 config.Format = value
501 return v1beta1.CustomResourceColumnDefinition{}, fmt.Errorf("invalid value for %s printcolumn", printColumnFormat)
503 case printColumnPath:
504 config.JSONPath = value
506 i, err := strconv.Atoi(value)
509 return v1beta1.CustomResourceColumnDefinition{}, fmt.Errorf("invalid value for %s printcolumn", printColumnPri)
512 case printColumnDescr:
513 config.Description = value
515 return v1beta1.CustomResourceColumnDefinition{}, fmt.Errorf(printColumnError)
519 return v1beta1.CustomResourceColumnDefinition{}, fmt.Errorf(printColumnError)
524 // printcolumn requires name,type,JSONPath fields and rest of the field are optional
525 // +kubebuilder:printcolumn:name=<name>,type=<type>,description=<desc>,JSONPath:<.spec.Name>,priority=<int32>,format=<format>
526 func parsePrintColumnParams(t *types.Type) ([]v1beta1.CustomResourceColumnDefinition, error) {
527 result := []v1beta1.CustomResourceColumnDefinition{}
528 for _, comment := range t.CommentLines {
529 if strings.Contains(comment, "+kubebuilder:printcolumn") {
530 parts := strings.Replace(comment, "+kubebuilder:printcolumn:", "", -1)
531 res, err := helperPrintColumn(parts, comment)
533 return []v1beta1.CustomResourceColumnDefinition{}, err
535 result = append(result, res)