1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package fielddesc
16
17 import (
18 "fmt"
19 "sort"
20
21 "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s"
22
23 apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
24 )
25
26 type RequirementLevel string
27
28 const (
29 OptionalRequirementLevel = "Optional"
30 RequiredWhenParentPresentRequirementLevel = "RequiredWhenParentPresent"
31 RequiredRequirementLevel = "Required"
32 )
33
34 type FieldDescription struct {
35 FullName []string
36 ShortName string
37 Description string
38 Type string
39 RequirementLevel RequirementLevel
40 Children []FieldDescription
41 AdditionalProperties []FieldDescription
42 }
43
44 func GetSpecDescription(crd *apiextensions.CustomResourceDefinition) FieldDescription {
45 crdDesc := getCRDFieldDescription(crd)
46 spec, ok := getChildFieldDesc(crdDesc, "spec")
47 if !ok {
48
49 return FieldDescription{
50 Type: "object",
51 RequirementLevel: OptionalRequirementLevel,
52 Children: make([]FieldDescription, 0),
53 }
54 }
55 return *spec
56 }
57
58 func GetStatusDescription(crd *apiextensions.CustomResourceDefinition) (FieldDescription, error) {
59 statusPropertyName := "status"
60 crdDesc := getCRDFieldDescription(crd)
61 status, ok := getChildFieldDesc(crdDesc, statusPropertyName)
62 if !ok {
63 return FieldDescription{}, fmt.Errorf("unexpected missing '%v' on crd '%v'", statusPropertyName, crd.Spec.Names.Kind)
64 }
65 return *status, nil
66 }
67
68 func getChildFieldDesc(description FieldDescription, childName string) (*FieldDescription, bool) {
69 for _, c := range description.Children {
70 if c.ShortName == childName {
71 return &c, true
72 }
73 }
74 return nil, false
75 }
76
77 func getCRDFieldDescription(crd *apiextensions.CustomResourceDefinition) FieldDescription {
78 customResourceDesc := FieldDescription{
79 Type: "object",
80 RequirementLevel: RequiredRequirementLevel,
81 }
82 schema := k8s.GetOpenAPIV3SchemaFromCRD(crd)
83 return propsToDescription(*schema, customResourceDesc, "", true)
84 }
85
86 func propsToDescription(props apiextensions.JSONSchemaProps, parent FieldDescription, name string, required bool) FieldDescription {
87 switch props.Type {
88 case "object":
89 return objectToDescription(props, parent, name, required)
90 case "array":
91 return sliceToDescriptions(props, parent, name, required)
92 case "boolean", "integer", "string", "number":
93 return newFieldDescription(props, parent, name, required)
94 default:
95 panic(fmt.Sprintf("unhandled type: %v", props.Type))
96 }
97 }
98
99 func sliceToDescriptions(props apiextensions.JSONSchemaProps, parent FieldDescription, name string, required bool) FieldDescription {
100 propsItemSchema := *props.Items.Schema
101 fd := newFieldDescription(props, parent, name, required)
102 fd.Type = fmt.Sprintf("list (%v)", propsItemSchema.Type)
103 fd.Children = []FieldDescription{propsToDescription(propsItemSchema, fd, "[]", required)}
104 return fd
105 }
106
107 func objectToDescription(props apiextensions.JSONSchemaProps, parent FieldDescription, name string, required bool) FieldDescription {
108 fd := newFieldDescription(props, parent, name, required)
109 isMap := isMapType(props)
110 if isMap {
111 supportedTypes := map[string]bool{
112 "boolean": true,
113 "integer": true,
114 "string": true,
115 "number": true,
116 "object": true,
117 }
118 valueType := props.AdditionalProperties.Schema.Type
119 if _, ok := supportedTypes[valueType]; !ok {
120 panic("only support maps of boolean, integer, string, number, and object types")
121 }
122
123 fd.Type = fmt.Sprintf("map (key: string, value: %v)", valueType)
124 if valueType != "object" {
125 return fd
126 }
127
128 props = *props.AdditionalProperties.Schema
129 }
130 requiredFields := make(map[string]bool)
131 for _, s := range props.Required {
132 requiredFields[s] = true
133 }
134 keys := make([]string, 0, len(props.Properties))
135 for k := range props.Properties {
136 keys = append(keys, k)
137 }
138 sort.Strings(keys)
139 for _, k := range keys {
140 v := props.Properties[k]
141 if isMap {
142 fd.AdditionalProperties = append(fd.AdditionalProperties, propsToDescription(v, fd, k, requiredFields[k]))
143 } else {
144 fd.Children = append(fd.Children, propsToDescription(v, fd, k, requiredFields[k]))
145 }
146 }
147 return fd
148 }
149
150 func newFieldDescription(props apiextensions.JSONSchemaProps, parent FieldDescription, name string, required bool) FieldDescription {
151 fullName := make([]string, len(parent.FullName), len(parent.FullName)+1)
152 copy(fullName, parent.FullName)
153 if name != "" {
154 fullName = append(fullName, name)
155 }
156 fd := FieldDescription{
157 Type: props.Type,
158 Description: props.Description,
159 FullName: fullName,
160 ShortName: name,
161 }
162 if fd.Type == "number" {
163 fd.Type = "float"
164 }
165 if required {
166 switch parent.RequirementLevel {
167 case RequiredRequirementLevel:
168 fd.RequirementLevel = RequiredRequirementLevel
169 case RequiredWhenParentPresentRequirementLevel, OptionalRequirementLevel:
170 fd.RequirementLevel = RequiredWhenParentPresentRequirementLevel
171 default:
172 panic(fmt.Errorf("unhandled requirement level: %v", parent.RequirementLevel))
173 }
174 } else {
175 fd.RequirementLevel = OptionalRequirementLevel
176 }
177 return fd
178 }
179
180 func isMapType(props apiextensions.JSONSchemaProps) bool {
181
182 return props.AdditionalProperties != nil && props.AdditionalProperties.Allows
183 }
184
View as plain text