k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/generators/extension.go (about)

     1  /*
     2  Copyright 2018 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 generators
    18  
    19  import (
    20  	"fmt"
    21  	"sort"
    22  	"strings"
    23  
    24  	"k8s.io/gengo/v2"
    25  	"k8s.io/gengo/v2/types"
    26  	"k8s.io/kube-openapi/pkg/util/sets"
    27  )
    28  
    29  const extensionPrefix = "x-kubernetes-"
    30  
    31  // extensionAttributes encapsulates common traits for particular extensions.
    32  type extensionAttributes struct {
    33  	xName         string
    34  	kind          types.Kind
    35  	allowedValues sets.String
    36  	enforceArray  bool
    37  }
    38  
    39  // Extension tag to openapi extension attributes
    40  var tagToExtension = map[string]extensionAttributes{
    41  	"patchMergeKey": {
    42  		xName: "x-kubernetes-patch-merge-key",
    43  		kind:  types.Slice,
    44  	},
    45  	"patchStrategy": {
    46  		xName:         "x-kubernetes-patch-strategy",
    47  		kind:          types.Slice,
    48  		allowedValues: sets.NewString("merge", "retainKeys"),
    49  	},
    50  	"listMapKey": {
    51  		xName:        "x-kubernetes-list-map-keys",
    52  		kind:         types.Slice,
    53  		enforceArray: true,
    54  	},
    55  	"listType": {
    56  		xName:         "x-kubernetes-list-type",
    57  		kind:          types.Slice,
    58  		allowedValues: sets.NewString("atomic", "set", "map"),
    59  	},
    60  	"mapType": {
    61  		xName:         "x-kubernetes-map-type",
    62  		kind:          types.Map,
    63  		allowedValues: sets.NewString("atomic", "granular"),
    64  	},
    65  	"structType": {
    66  		xName:         "x-kubernetes-map-type",
    67  		kind:          types.Struct,
    68  		allowedValues: sets.NewString("atomic", "granular"),
    69  	},
    70  	"validations": {
    71  		xName: "x-kubernetes-validations",
    72  		kind:  types.Slice,
    73  	},
    74  }
    75  
    76  // Extension encapsulates information necessary to generate an OpenAPI extension.
    77  type extension struct {
    78  	idlTag string   // Example: listType
    79  	xName  string   // Example: x-kubernetes-list-type
    80  	values []string // Example: [atomic]
    81  }
    82  
    83  func (e extension) hasAllowedValues() bool {
    84  	return tagToExtension[e.idlTag].allowedValues.Len() > 0
    85  }
    86  
    87  func (e extension) allowedValues() sets.String {
    88  	return tagToExtension[e.idlTag].allowedValues
    89  }
    90  
    91  func (e extension) hasKind() bool {
    92  	return len(tagToExtension[e.idlTag].kind) > 0
    93  }
    94  
    95  func (e extension) kind() types.Kind {
    96  	return tagToExtension[e.idlTag].kind
    97  }
    98  
    99  func (e extension) validateAllowedValues() error {
   100  	// allowedValues not set means no restrictions on values.
   101  	if !e.hasAllowedValues() {
   102  		return nil
   103  	}
   104  	// Check for missing value.
   105  	if len(e.values) == 0 {
   106  		return fmt.Errorf("%s needs a value, none given.", e.idlTag)
   107  	}
   108  	// For each extension value, validate that it is allowed.
   109  	allowedValues := e.allowedValues()
   110  	if !allowedValues.HasAll(e.values...) {
   111  		return fmt.Errorf("%v not allowed for %s. Allowed values: %v",
   112  			e.values, e.idlTag, allowedValues.List())
   113  	}
   114  	return nil
   115  }
   116  
   117  func (e extension) validateType(kind types.Kind) error {
   118  	// If this extension class has no kind, then don't validate the type.
   119  	if !e.hasKind() {
   120  		return nil
   121  	}
   122  	if kind != e.kind() {
   123  		return fmt.Errorf("tag %s on type %v; only allowed on type %v",
   124  			e.idlTag, kind, e.kind())
   125  	}
   126  	return nil
   127  }
   128  
   129  func (e extension) hasMultipleValues() bool {
   130  	return len(e.values) > 1
   131  }
   132  
   133  func (e extension) isAlwaysArrayFormat() bool {
   134  	return tagToExtension[e.idlTag].enforceArray
   135  }
   136  
   137  // Returns sorted list of map keys. Needed for deterministic testing.
   138  func sortedMapKeys(m map[string][]string) []string {
   139  	keys := make([]string, len(m))
   140  	i := 0
   141  	for k := range m {
   142  		keys[i] = k
   143  		i++
   144  	}
   145  	sort.Strings(keys)
   146  	return keys
   147  }
   148  
   149  // Parses comments to return openapi extensions. Returns a list of
   150  // extensions which parsed correctly, as well as a list of the
   151  // parse errors. Validating extensions is performed separately.
   152  // NOTE: Non-empty errors does not mean extensions is empty.
   153  func parseExtensions(comments []string) ([]extension, []error) {
   154  	extensions := []extension{}
   155  	errors := []error{}
   156  	// First, generate extensions from "+k8s:openapi-gen=x-kubernetes-*" annotations.
   157  	values := getOpenAPITagValue(comments)
   158  	for _, val := range values {
   159  		// Example: x-kubernetes-member-tag:member_test
   160  		if strings.HasPrefix(val, extensionPrefix) {
   161  			parts := strings.SplitN(val, ":", 2)
   162  			if len(parts) != 2 {
   163  				errors = append(errors, fmt.Errorf("invalid extension value: %v", val))
   164  				continue
   165  			}
   166  			e := extension{
   167  				idlTag: tagName,            // Example: k8s:openapi-gen
   168  				xName:  parts[0],           // Example: x-kubernetes-member-tag
   169  				values: []string{parts[1]}, // Example: member_test
   170  			}
   171  			extensions = append(extensions, e)
   172  		}
   173  	}
   174  	// Next, generate extensions from "idlTags" (e.g. +listType)
   175  	tagValues := gengo.ExtractCommentTags("+", comments)
   176  	for _, idlTag := range sortedMapKeys(tagValues) {
   177  		xAttrs, exists := tagToExtension[idlTag]
   178  		if !exists {
   179  			continue
   180  		}
   181  		values := tagValues[idlTag]
   182  		e := extension{
   183  			idlTag: idlTag,       // listType
   184  			xName:  xAttrs.xName, // x-kubernetes-list-type
   185  			values: values,       // [atomic]
   186  		}
   187  		extensions = append(extensions, e)
   188  	}
   189  	return extensions, errors
   190  }
   191  
   192  func validateMemberExtensions(extensions []extension, m *types.Member) []error {
   193  	errors := []error{}
   194  	for _, e := range extensions {
   195  		if err := e.validateAllowedValues(); err != nil {
   196  			errors = append(errors, err)
   197  		}
   198  		if err := e.validateType(m.Type.Kind); err != nil {
   199  			errors = append(errors, err)
   200  		}
   201  	}
   202  	return errors
   203  }