Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions internal/crd/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}

Expand Down
109 changes: 109 additions & 0 deletions internal/crd/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package crd

import (
"sort"
"testing"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -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()

Expand Down
Loading