...

Source file src/sigs.k8s.io/gateway-api/pkg/generator/main.go

Documentation: sigs.k8s.io/gateway-api/pkg/generator

     1  /*
     2  Copyright 2021 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 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  	// These values must be updated during the release process
    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  // This generation code is largely copied from
    50  // github.com/kubernetes-sigs/controller-tools/blob/ab52f76cc7d167925b2d5942f24bf22e30f49a02/pkg/crd/gen.go
    51  func main() {
    52  	roots, err := loader.LoadRoots(
    53  		"k8s.io/apimachinery/pkg/runtime/schema", // Needed to parse generated register functions.
    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  			// Inline version of "addAttribution(&crdRaw)" ...
   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  			// Prevent the top level metadata for the CRD to be generated regardless of the intention in the arguments
   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  // Custom Gateway API Tweaks for tags prefixed with `<gateway:` that get past
   139  // the limitations of Kubebuilder annotations.
   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  		// TODO(robscott): Figure out why crdgen switched this to "object"
   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