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