1
16
17 package main
18
19 import (
20 "fmt"
21 "log"
22 "os"
23 "regexp"
24 "strings"
25
26 apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
27 "sigs.k8s.io/controller-tools/pkg/crd"
28 "sigs.k8s.io/controller-tools/pkg/loader"
29 "sigs.k8s.io/controller-tools/pkg/markers"
30 "sigs.k8s.io/yaml"
31 )
32
33 const (
34 bundleVersionAnnotation = "gateway.networking.k8s.io/bundle-version"
35 channelAnnotation = "gateway.networking.k8s.io/channel"
36
37
38 bundleVersion = "v1.0.0"
39 approvalLink = "https://github.com/kubernetes-sigs/gateway-api/pull/2466"
40 )
41
42 var standardKinds = map[string]bool{
43 "GatewayClass": true,
44 "Gateway": true,
45 "HTTPRoute": true,
46 "ReferenceGrant": true,
47 }
48
49
50
51 func main() {
52 roots, err := loader.LoadRoots(
53 "k8s.io/apimachinery/pkg/runtime/schema",
54 "sigs.k8s.io/gateway-api/apis/v1alpha2",
55 "sigs.k8s.io/gateway-api/apis/v1beta1",
56 "sigs.k8s.io/gateway-api/apis/v1",
57 )
58 if err != nil {
59 log.Fatalf("failed to load package roots: %s", err)
60 }
61
62 generator := &crd.Generator{}
63
64 parser := &crd.Parser{
65 Collector: &markers.Collector{Registry: &markers.Registry{}},
66 Checker: &loader.TypeChecker{
67 NodeFilters: []loader.NodeFilter{generator.CheckFilter()},
68 },
69 }
70
71 err = generator.RegisterMarkers(parser.Collector.Registry)
72 if err != nil {
73 log.Fatalf("failed to register markers: %s", err)
74 }
75
76 crd.AddKnownTypes(parser)
77 for _, r := range roots {
78 parser.NeedPackage(r)
79 }
80
81 metav1Pkg := crd.FindMetav1(roots)
82 if metav1Pkg == nil {
83 log.Fatalf("no objects in the roots, since nothing imported metav1")
84 }
85
86 kubeKinds := crd.FindKubeKinds(parser, metav1Pkg)
87 if len(kubeKinds) == 0 {
88 log.Fatalf("no objects in the roots")
89 }
90
91 channels := []string{"standard", "experimental"}
92 for _, channel := range channels {
93 for _, groupKind := range kubeKinds {
94 if channel == "standard" && !standardKinds[groupKind.Kind] {
95 continue
96 }
97
98 log.Printf("generating %s CRD for %v\n", channel, groupKind)
99
100 parser.NeedCRDFor(groupKind, nil)
101 crdRaw := parser.CustomResourceDefinitions[groupKind]
102
103
104 if crdRaw.ObjectMeta.Annotations == nil {
105 crdRaw.ObjectMeta.Annotations = map[string]string{}
106 }
107 crdRaw.ObjectMeta.Annotations[bundleVersionAnnotation] = bundleVersion
108 crdRaw.ObjectMeta.Annotations[channelAnnotation] = channel
109 crdRaw.ObjectMeta.Annotations[apiext.KubeAPIApprovedAnnotation] = approvalLink
110
111
112 crd.FixTopLevelMetadata(crdRaw)
113
114 channelCrd := crdRaw.DeepCopy()
115 for _, version := range channelCrd.Spec.Versions {
116 version.Schema.OpenAPIV3Schema.Properties = gatewayTweaks(channel, version.Schema.OpenAPIV3Schema.Properties)
117 }
118
119 conv, err := crd.AsVersion(*channelCrd, apiext.SchemeGroupVersion)
120 if err != nil {
121 log.Fatalf("failed to convert CRD: %s", err)
122 }
123
124 out, err := yaml.Marshal(conv)
125 if err != nil {
126 log.Fatalf("failed to marshal CRD: %s", err)
127 }
128
129 fileName := fmt.Sprintf("config/crd/%s/%s_%s.yaml", channel, crdRaw.Spec.Group, crdRaw.Spec.Names.Plural)
130 err = os.WriteFile(fileName, out, 0o600)
131 if err != nil {
132 log.Fatalf("failed to write CRD: %s", err)
133 }
134 }
135 }
136 }
137
138
139
140 func gatewayTweaks(channel string, props map[string]apiext.JSONSchemaProps) map[string]apiext.JSONSchemaProps {
141 for name := range props {
142 jsonProps, _ := props[name]
143
144 if strings.Contains(jsonProps.Description, "<gateway:validateIPAddress>") {
145 jsonProps.Items.Schema.OneOf = []apiext.JSONSchemaProps{{
146 Properties: map[string]apiext.JSONSchemaProps{
147 "type": {
148 Enum: []apiext.JSON{{Raw: []byte("\"IPAddress\"")}},
149 },
150 "value": {
151 AnyOf: []apiext.JSONSchemaProps{{
152 Format: "ipv4",
153 }, {
154 Format: "ipv6",
155 }},
156 },
157 },
158 }, {
159 Properties: map[string]apiext.JSONSchemaProps{
160 "type": {
161 Not: &apiext.JSONSchemaProps{
162 Enum: []apiext.JSON{{Raw: []byte("\"IPAddress\"")}},
163 },
164 },
165 },
166 }}
167 }
168
169 if channel == "standard" && strings.Contains(jsonProps.Description, "<gateway:experimental>") {
170 delete(props, name)
171 continue
172 }
173
174
175 if jsonProps.Format == "date-time" {
176 jsonProps.Type = "string"
177 }
178
179 validationPrefix := fmt.Sprintf("<gateway:%s:validation:", channel)
180 numExpressions := strings.Count(jsonProps.Description, validationPrefix)
181 numValid := 0
182 if numExpressions > 0 {
183 enumRe := regexp.MustCompile(validationPrefix + "Enum=([A-Za-z;]*)>")
184 enumMatches := enumRe.FindAllStringSubmatch(jsonProps.Description, 64)
185 for _, enumMatch := range enumMatches {
186 if len(enumMatch) != 2 {
187 log.Fatalf("Invalid %s Enum tag for %s", validationPrefix, name)
188 }
189
190 numValid++
191 jsonProps.Enum = []apiext.JSON{}
192 for _, val := range strings.Split(enumMatch[1], ";") {
193 jsonProps.Enum = append(jsonProps.Enum, apiext.JSON{Raw: []byte("\"" + val + "\"")})
194 }
195 }
196
197 celRe := regexp.MustCompile(validationPrefix + "XValidation:message=\"([^\"]*)\",rule=\"([^\"]*)\">")
198 celMatches := celRe.FindAllStringSubmatch(jsonProps.Description, 64)
199 for _, celMatch := range celMatches {
200 if len(celMatch) != 3 {
201 log.Fatalf("Invalid %s CEL tag for %s", validationPrefix, name)
202 }
203
204 numValid++
205 jsonProps.XValidations = append(jsonProps.XValidations, apiext.ValidationRule{
206 Message: celMatch[1],
207 Rule: celMatch[2],
208 })
209 }
210 }
211 startTag := "<gateway:experimental:description>"
212 endTag := "</gateway:experimental:description>"
213 regexPattern := regexp.QuoteMeta(startTag) + `(?s:(.*?))` + regexp.QuoteMeta(endTag)
214 if channel == "standard" && strings.Contains(jsonProps.Description, "<gateway:experimental:description>") {
215 re := regexp.MustCompile(regexPattern)
216 match := re.FindStringSubmatch(jsonProps.Description)
217 if len(match) != 2 {
218 log.Fatalf("Invalid <gateway:experimental:description> tag for %s", name)
219 }
220 modifiedDescription := re.ReplaceAllString(jsonProps.Description, "")
221 jsonProps.Description = modifiedDescription
222 } else {
223 jsonProps.Description = strings.ReplaceAll(jsonProps.Description, startTag, "")
224 jsonProps.Description = strings.ReplaceAll(jsonProps.Description, endTag, "")
225 }
226
227 if numValid < numExpressions {
228 fmt.Printf("Description: %s\n", jsonProps.Description)
229 log.Fatalf("Found %d Gateway validation expressions, but only %d were valid", numExpressions, numValid)
230 }
231
232 gatewayRe := regexp.MustCompile(`<gateway:.*>`)
233 jsonProps.Description = gatewayRe.ReplaceAllLiteralString(jsonProps.Description, "")
234
235 if len(jsonProps.Properties) > 0 {
236 jsonProps.Properties = gatewayTweaks(channel, jsonProps.Properties)
237 } else if jsonProps.Items != nil && jsonProps.Items.Schema != nil {
238 jsonProps.Items.Schema.Properties = gatewayTweaks(channel, jsonProps.Items.Schema.Properties)
239 }
240 props[name] = jsonProps
241 }
242 return props
243 }
244
View as plain text