From 809565bd29f3aac41cf8d53d3e3957c1844bac2a Mon Sep 17 00:00:00 2001 From: Nic Cope Date: Mon, 15 Jun 2026 15:28:59 -0700 Subject: [PATCH] Drop the scale subresource before building OpenAPI for schema generation Crossplane v2.3 lets XRDs configure a scale subresource. When a resource declares one, generating language schemas for it produces models of the autoscaling/v1 Scale type in place of the resource's own model. For Python the generated module's imports also change shape, so importing the model fails outright with "No module named 'models.ai.apimachinery'". ToOpenAPI builds the intermediate OpenAPI document with BuildOpenAPIV3, which emits the resource's whole REST API surface, not just its structural schema. Declaring a scale subresource adds a /scale endpoint whose request and response body is an autoscaling/v1 Scale. To describe that endpoint the builder adds the Scale, ScaleSpec, and ScaleStatus schemas to the document's components. The meta types it always emits (ObjectMeta and friends) are redirected to a shared module by a later transform, but the autoscaling types are not, so the language generator models them into the resource's own module. The scale subresource is a runtime API behaviour, not part of the resource's structural schema, so it has no bearing on the generated model. This clears it before the conversion, so BuildOpenAPIV3 never emits the /scale endpoint or its Scale schemas and the resource's model generates as it did before. Fixes #117. Signed-off-by: Nic Cope --- internal/crd/convert.go | 11 ++++ internal/crd/convert_test.go | 109 +++++++++++++++++++++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/internal/crd/convert.go b/internal/crd/convert.go index 231a9051..82c63626 100644 --- a/internal/crd/convert.go +++ b/internal/crd/convert.go @@ -128,6 +128,17 @@ func modifyCRDManifestFields(crd *extv1.CustomResourceDefinition) { updateSchemaPropertiesXEmbeddedResource(version.Schema.OpenAPIV3Schema) crd.Spec.Versions[i].Schema.OpenAPIV3Schema.Properties = version.Schema.OpenAPIV3Schema.Properties } + + // The scale subresource is a runtime API behaviour, not part of the + // resource's structural schema. But BuildOpenAPIV3 models the whole REST + // surface, so declaring it adds a /scale endpoint whose request and + // response body is an autoscaling/v1 Scale. That pulls the Scale, + // ScaleSpec, and ScaleStatus schemas into the resource's components, + // which the language generators then model in place of the resource + // itself. Drop it before building the OpenAPI spec. + if version.Subresources != nil { + crd.Spec.Versions[i].Subresources.Scale = nil + } } } diff --git a/internal/crd/convert_test.go b/internal/crd/convert_test.go index 7bf9aefe..efee70ff 100644 --- a/internal/crd/convert_test.go +++ b/internal/crd/convert_test.go @@ -17,6 +17,7 @@ limitations under the License. package crd import ( + "sort" "testing" "github.com/google/go-cmp/cmp" @@ -112,6 +113,114 @@ spec: } } +func TestToOpenAPI(t *testing.T) { + t.Parallel() + + scaleCRD := []byte(` +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: testresources.testgroup.example.com +spec: + group: testgroup.example.com + names: + kind: TestResource + plural: testresources + scope: Namespaced + versions: + - name: v1 + served: true + storage: true + subresources: + status: {} + scale: + specReplicasPath: .spec.replicas + statusReplicasPath: .status.replicas + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + replicas: + type: integer + status: + type: object + properties: + replicas: + type: integer +`) + + tests := []struct { + name string + crdContent []byte + version string + // wantSchemas is the exact set of component schema names the version's + // OpenAPI document should contain, sorted. + wantSchemas []string + }{ + { + // The scale subresource adds a /scale endpoint whose request and + // response body is an autoscaling/v1 Scale. Building the OpenAPI for + // it must not pull the Scale types into the resource's components, + // where they would be modelled in place of the resource itself. The + // document holds the resource, its list, and the meta types the + // builder always emits - and nothing from autoscaling/v1. + name: "ScaleSubresourceDoesNotLeakAutoscalingTypes", + crdContent: scaleCRD, + version: "v1", + wantSchemas: []string{ + "com.example.testgroup.v1.", + "com.example.testgroup.v1.TestResource", + "io.k8s.apimachinery.pkg.apis.meta.v1.DeleteOptions", + "io.k8s.apimachinery.pkg.apis.meta.v1.FieldsV1", + "io.k8s.apimachinery.pkg.apis.meta.v1.ListMeta", + "io.k8s.apimachinery.pkg.apis.meta.v1.ManagedFieldsEntry", + "io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta", + "io.k8s.apimachinery.pkg.apis.meta.v1.OwnerReference", + "io.k8s.apimachinery.pkg.apis.meta.v1.Patch", + "io.k8s.apimachinery.pkg.apis.meta.v1.Preconditions", + "io.k8s.apimachinery.pkg.apis.meta.v1.Status", + "io.k8s.apimachinery.pkg.apis.meta.v1.StatusCause", + "io.k8s.apimachinery.pkg.apis.meta.v1.StatusDetails", + "io.k8s.apimachinery.pkg.apis.meta.v1.Time", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + var crd extv1.CustomResourceDefinition + if err := yaml.Unmarshal(tt.crdContent, &crd); err != nil { + t.Fatalf("failed to unmarshal CRD: %v", err) + } + + out, err := ToOpenAPI(&crd) + if err != nil { + t.Fatalf("ToOpenAPI() error: %v", err) + } + + oapi, ok := out[tt.version] + if !ok { + t.Fatalf("ToOpenAPI() returned no %q output", tt.version) + } + + gotSchemas := make([]string, 0, len(oapi.Components.Schemas)) + for name := range oapi.Components.Schemas { + gotSchemas = append(gotSchemas, name) + } + sort.Strings(gotSchemas) + + if diff := cmp.Diff(tt.wantSchemas, gotSchemas); diff != "" { + t.Errorf("ToOpenAPI() component schemas (-want +got):\n%s", diff) + } + }) + } +} + func TestAddDefaultAPIVersionAndKind(t *testing.T) { t.Parallel()