From 52c917101b4edb0de3c3029c67b7dbf5ade5ae31 Mon Sep 17 00:00:00 2001 From: justinsb Date: Wed, 30 Oct 2024 17:19:48 -0400 Subject: [PATCH] chore: update client generation to generate multiple CRD versions --- pkg/crd/fielddesc/fielddesc.go | 14 +++--- pkg/crd/fielddesc/fielddesc_test.go | 28 ++++++----- pkg/k8s/crds.go | 11 ++++- .../generate-types-file.go | 48 ++++++++++++------- .../resource-reference/main.go | 8 ++-- 5 files changed, 68 insertions(+), 41 deletions(-) diff --git a/pkg/crd/fielddesc/fielddesc.go b/pkg/crd/fielddesc/fielddesc.go index 93b1588798..edd4ee0c70 100644 --- a/pkg/crd/fielddesc/fielddesc.go +++ b/pkg/crd/fielddesc/fielddesc.go @@ -42,8 +42,8 @@ type FieldDescription struct { AdditionalProperties []FieldDescription } -func GetSpecDescription(crd *apiextensions.CustomResourceDefinition) FieldDescription { - crdDesc := getCRDFieldDescription(crd) +func GetSpecDescription(crd *apiextensions.CustomResourceDefinition, version string) FieldDescription { + crdDesc := getCRDFieldDescription(crd, version) spec, ok := getChildFieldDesc(crdDesc, "spec") if !ok { // this occurs when a CRD has an empty spec, such as ComputeSharedVPCHostProject @@ -56,9 +56,9 @@ func GetSpecDescription(crd *apiextensions.CustomResourceDefinition) FieldDescri return *spec } -func GetStatusDescription(crd *apiextensions.CustomResourceDefinition) (FieldDescription, error) { +func GetStatusDescription(crd *apiextensions.CustomResourceDefinition, version string) (FieldDescription, error) { statusPropertyName := "status" - crdDesc := getCRDFieldDescription(crd) + crdDesc := getCRDFieldDescription(crd, version) status, ok := getChildFieldDesc(crdDesc, statusPropertyName) if !ok { return FieldDescription{}, fmt.Errorf("unexpected missing '%v' on crd '%v'", statusPropertyName, crd.Spec.Names.Kind) @@ -75,13 +75,13 @@ func getChildFieldDesc(description FieldDescription, childName string) (*FieldDe return nil, false } -func getCRDFieldDescription(crd *apiextensions.CustomResourceDefinition) FieldDescription { +func getCRDFieldDescription(crd *apiextensions.CustomResourceDefinition, version string) FieldDescription { customResourceDesc := FieldDescription{ Type: "object", RequirementLevel: RequiredRequirementLevel, } - schema := k8s.GetOpenAPIV3SchemaFromCRD(crd) - return propsToDescription(*schema, customResourceDesc, "", true) + crdVersionDefinition := k8s.GetCRDVersionDefinition(crd, version) + return propsToDescription(*crdVersionDefinition.Schema.OpenAPIV3Schema, customResourceDesc, "", true) } func propsToDescription(props apiextensions.JSONSchemaProps, parent FieldDescription, name string, required bool) FieldDescription { diff --git a/pkg/crd/fielddesc/fielddesc_test.go b/pkg/crd/fielddesc/fielddesc_test.go index 462ef5d34f..af74acdd8b 100644 --- a/pkg/crd/fielddesc/fielddesc_test.go +++ b/pkg/crd/fielddesc/fielddesc_test.go @@ -21,6 +21,7 @@ import ( "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/crd/crdloader" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/crd/fielddesc" + "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/test" "gopkg.in/yaml.v2" @@ -33,14 +34,16 @@ func TestAllCRDsGetSpecAndStatusDescription(t *testing.T) { t.Fatalf("error loading crds: %v", err) } for _, crd := range crds { - fd := fielddesc.GetSpecDescription(&crd) - expectedType := "object" - if fd.Type != expectedType { - t.Fatalf("unexpected type: got '%v', want' %v'", fd.Type, expectedType) - } - fd = getStatusDescription(t, &crd) - if fd.Type != expectedType { - t.Fatalf("unexpected type: got '%v', want' %v'", fd.Type, expectedType) + for _, version := range crd.Spec.Versions { + fd := fielddesc.GetSpecDescription(&crd, version.Name) + expectedType := "object" + if fd.Type != expectedType { + t.Fatalf("unexpected type: got '%v', want' %v'", fd.Type, expectedType) + } + fd = getStatusDescription(t, &crd, version.Name) + if fd.Type != expectedType { + t.Fatalf("unexpected type: got '%v', want' %v'", fd.Type, expectedType) + } } } } @@ -58,16 +61,17 @@ func testOutputMatches(t *testing.T, resourceKind string) { if err != nil { t.Fatalf("error getting crd '%v': %v", resourceKind, err) } - fd := fielddesc.GetSpecDescription(crd) + version := k8s.PreferredVersion(crd) + fd := fielddesc.GetSpecDescription(crd, version.Name) fieldDescYAML := fieldDescToYAML(t, fd) test.CompareGoldenFile(t, fmt.Sprintf("testdata/%v-spec.golden.yaml", strings.ToLower(resourceKind)), string(fieldDescYAML), test.IgnoreLeadingComments) - fd = getStatusDescription(t, crd) + fd = getStatusDescription(t, crd, version.Name) fieldDescYAML = fieldDescToYAML(t, fd) test.CompareGoldenFile(t, fmt.Sprintf("testdata/%v-status.golden.yaml", strings.ToLower(resourceKind)), string(fieldDescYAML), test.IgnoreLeadingComments) } -func getStatusDescription(t *testing.T, crd *apiextensions.CustomResourceDefinition) fielddesc.FieldDescription { - fd, err := fielddesc.GetStatusDescription(crd) +func getStatusDescription(t *testing.T, crd *apiextensions.CustomResourceDefinition, version string) fielddesc.FieldDescription { + fd, err := fielddesc.GetStatusDescription(crd, version) if err != nil { t.Fatalf("error getting status description") } diff --git a/pkg/k8s/crds.go b/pkg/k8s/crds.go index 1be092f920..8fbe02afbc 100644 --- a/pkg/k8s/crds.go +++ b/pkg/k8s/crds.go @@ -65,8 +65,17 @@ func GetVersionFromCRD(crd *apiextensions.CustomResourceDefinition) string { return PreferredVersion(crd).Name } +func GetCRDVersionDefinition(crd *apiextensions.CustomResourceDefinition, version string) *apiextensions.CustomResourceDefinitionVersion { + for _, v := range crd.Spec.Versions { + if v.Name == version { + return &v + } + } + panic(fmt.Sprintf("version %q not found", version)) +} + +// Deprecated: only returns the preferred version func GetOpenAPIV3SchemaFromCRD(crd *apiextensions.CustomResourceDefinition) *apiextensions.JSONSchemaProps { - panicIfNoVersionPresent(crd) // Currently KCC CRDs only support one version. return PreferredVersion(crd).Schema.OpenAPIV3Schema } diff --git a/scripts/generate-go-crd-clients/generate-types-file.go b/scripts/generate-go-crd-clients/generate-types-file.go index dfff89b2a9..61c7c1b04b 100644 --- a/scripts/generate-go-crd-clients/generate-types-file.go +++ b/scripts/generate-go-crd-clients/generate-types-file.go @@ -33,6 +33,7 @@ import ( "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/k8s" "github.com/GoogleCloudPlatform/k8s-config-connector/pkg/util/repo" apiextensions "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/util/sets" "k8s.io/klog/v2" ) @@ -74,7 +75,7 @@ type svkMap struct { } func main() { - resources := make(map[string]*resourceDefinition) + var resources []*resourceDefinition registerKinds := make(map[string]*svkMap) crdsDir := repo.GetCRDsPath() crdsPath, err := filepath.Abs(crdsDir) @@ -87,8 +88,7 @@ func main() { } for _, crdFile := range crdFiles { - fileName := strings.TrimSuffix(crdFile.Name(), ".yaml") - resources[fileName] = constructResourceDefinition(crdsPath, crdFile.Name()) + resources = append(resources, constructResourceDefinitions(crdsPath, crdFile.Name())...) } for _, rd := range resources { @@ -261,8 +261,7 @@ func checkAndCreateFolder(dir string) { } } -func constructResourceDefinition(crdsPath, crdFile string) *resourceDefinition { - r := &resourceDefinition{} +func constructResourceDefinitions(crdsPath, crdFile string) []*resourceDefinition { crdFilePath, err := filepath.Abs(path.Join(crdsPath, crdFile)) if err != nil { log.Fatalf("error getting the absolute representation of path for directory '%v': %v", crdFile, err) @@ -272,25 +271,40 @@ func constructResourceDefinition(crdsPath, crdFile string) *resourceDefinition { log.Fatalf("error loading crd from filepath %v: %v", crdFilePath, err) } - r.CRD = crd - r.Name = crd.Spec.Names.Kind - if err = buildFieldProperties(r, crd); err != nil { - log.Fatalf("error building field properties for %v: %v", r.Name, err) + versionNames := sets.NewString() + for _, crdVersion := range crd.Spec.Versions { + versionNames.Insert(crdVersion.Name) } - r.Service = strings.TrimSuffix(crd.Spec.Group, k8s.APIDomainSuffix) - r.Kind = strings.ToLower(crd.Spec.Names.Kind) - // TODO: Should we handle multiple versions? - r.Version = k8s.PreferredVersion(crd) - return r + var resources []*resourceDefinition + for _, versionName := range versionNames.List() { + // Don't generate alpha version if we have a beta + if versionName == "v1alpha1" && versionNames.Has("v1beta1") { + continue + } + crdVersionDefinition := k8s.GetCRDVersionDefinition(crd, versionName) + + r := &resourceDefinition{} + r.CRD = crd + r.Name = crd.Spec.Names.Kind + if err = buildFieldProperties(r, crd, crdVersionDefinition.Name); err != nil { + log.Fatalf("error building field properties for %v: %v", r.Name, err) + } + r.Service = strings.TrimSuffix(crd.Spec.Group, k8s.APIDomainSuffix) + r.Kind = strings.ToLower(crd.Spec.Names.Kind) + + r.Version = crdVersionDefinition + resources = append(resources, r) + } + return resources } -func buildFieldProperties(r *resourceDefinition, crd *apiextensions.CustomResourceDefinition) error { - specDesc := fielddesc.GetSpecDescription(crd) +func buildFieldProperties(r *resourceDefinition, crd *apiextensions.CustomResourceDefinition, version string) error { + specDesc := fielddesc.GetSpecDescription(crd, version) specDescriptions := dropRootAndFlattenChildrenDescriptions(specDesc) r.SpecNestedStructs = make(map[string][]*fieldProperties) organizeSpecFieldDescriptions(specDescriptions, r) - statusDesc, err := fielddesc.GetStatusDescription(crd) + statusDesc, err := fielddesc.GetStatusDescription(crd, version) if err != nil { return fmt.Errorf("error getting status descriptions: %w", err) } diff --git a/scripts/generate-google3-docs/resource-reference/main.go b/scripts/generate-google3-docs/resource-reference/main.go index 8c4e4757d8..3b225dc390 100644 --- a/scripts/generate-google3-docs/resource-reference/main.go +++ b/scripts/generate-google3-docs/resource-reference/main.go @@ -286,7 +286,7 @@ func constructResourceForGVK(gvk schema.GroupVersionKind, smLoader *servicemappi return nil, fmt.Errorf("error converting status to YAML: %w", err) } r.Status = string(statusYaml) - if err = buildFieldDescriptions(r, crd); err != nil { + if err = buildFieldDescriptions(r, crd, gvk.Version); err != nil { return nil, fmt.Errorf("buildFieldDescriptions: %w", err) } r.DefaultReconcileInterval = uint32(reconciliationinterval.MeanReconcileReenqueuePeriod(gvk, smLoader, serviceMetadataLoader).Seconds()) @@ -545,12 +545,12 @@ func stripHeader(sample string) string { return strings.Trim(res, "\n") } -func buildFieldDescriptions(r *resource, crd *apiextensions.CustomResourceDefinition) error { - specDesc := fielddesc.GetSpecDescription(crd) +func buildFieldDescriptions(r *resource, crd *apiextensions.CustomResourceDefinition, version string) error { + specDesc := fielddesc.GetSpecDescription(crd, version) specDescriptions := dropRootAndFlattenChildrenDescriptions(specDesc) r.SpecDescriptions = fieldDescriptionsToHumanReadable(specDescriptions) r.SpecDescriptionContainsRequiredIfParentPresent = atLeastOneFieldHasRequiredWhenParentPresentRequirementLevel(specDesc) - statusDesc, err := fielddesc.GetStatusDescription(crd) + statusDesc, err := fielddesc.GetStatusDescription(crd, version) if err != nil { return fmt.Errorf("error getting status descriptions: %w", err) }