Skip to content

Commit accbda0

Browse files
fix(crd): panic when parsing type aliases from indirectly imported packages
1 parent b20dcc4 commit accbda0

5 files changed

Lines changed: 128 additions & 3 deletions

File tree

pkg/crd/parser_integration_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,29 @@ var _ = Describe("CRD Generation From Parsing to CustomResourceDefinition", func
277277
assertCRDForGroupKind(pkgs[0], schema.GroupKind{Kind: "CronJob"}, "testdata._cronjobs.yaml")
278278
})
279279
})
280+
281+
Context("Type Alias API", func() {
282+
BeforeEach(func() {
283+
pkgPaths = []string{"./typealias_indirect"}
284+
expPkgLen = 1
285+
})
286+
It("should handle types from indirectly imported packages via type aliases", func() {
287+
By("verifying no errors occurred during package loading and parsing")
288+
Expect(packageErrors(pkgs[0])).NotTo(HaveOccurred())
289+
290+
By("requesting schema for type with indirect alias")
291+
typeIdent := crd.TypeIdent{
292+
Package: pkgs[0],
293+
Name: "IndirectAliasSpec",
294+
}
295+
parser.NeedSchemaFor(typeIdent)
296+
297+
By("verifying the schema includes properties from the base type")
298+
schema, found := parser.Schemata[typeIdent]
299+
Expect(found).To(BeTrue(), "schema for IndirectAliasSpec should be generated")
300+
Expect(schema.Properties).To(HaveKey("field"), "should have field property")
301+
})
302+
})
280303
})
281304

282305
It("should generate plural words for Kind correctly", func() {

pkg/crd/schema.go

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,50 @@ func (c *schemaContext) ForInfo(info *markers.TypeInfo) *schemaContext {
102102
// requestSchema asks for the schema for a type in the package with the
103103
// given import path.
104104
func (c *schemaContext) requestSchema(pkgPath, typeName string) {
105+
c.requestSchemaWithPkg(pkgPath, typeName, nil)
106+
}
107+
108+
func (c *schemaContext) requestSchemaWithPkg(pkgPath, typeName string, typesPkg *types.Package) {
105109
pkg := c.pkg
106110
if pkgPath != "" {
107111
pkg = c.pkg.Imports()[pkgPath]
112+
if pkg == nil && typesPkg != nil {
113+
pkg = c.findPackageRecursive(typesPkg.Path())
114+
}
115+
if pkg == nil {
116+
c.pkg.AddError(fmt.Errorf("unable to find package %q for type %s (not in direct imports)", pkgPath, typeName))
117+
return
118+
}
108119
}
109120
c.schemaRequester.NeedSchemaFor(TypeIdent{
110121
Package: pkg,
111122
Name: typeName,
112123
})
113124
}
114125

126+
func (c *schemaContext) findPackageRecursive(pkgPath string) *loader.Package {
127+
visited := make(map[*loader.Package]bool)
128+
queue := []*loader.Package{c.pkg}
129+
130+
for len(queue) > 0 {
131+
current := queue[0]
132+
queue = queue[1:]
133+
134+
if visited[current] {
135+
continue
136+
}
137+
visited[current] = true
138+
139+
for importPath, importPkg := range current.Imports() {
140+
if loader.NonVendorPath(importPath) == loader.NonVendorPath(pkgPath) {
141+
return importPkg
142+
}
143+
queue = append(queue, importPkg)
144+
}
145+
}
146+
return nil
147+
}
148+
115149
// infoToSchema creates a schema for the type in the given set of type information.
116150
func infoToSchema(ctx *schemaContext) *apiextensionsv1.JSONSchemaProps {
117151
if obj := ctx.pkg.Types.Scope().Lookup(ctx.info.Name); obj != nil {
@@ -310,7 +344,7 @@ func localNamedToSchema(ctx *schemaContext, ident *ast.Ident) *apiextensionsv1.J
310344
if pkg == ctx.pkg.Types {
311345
pkgPath = ""
312346
}
313-
ctx.requestSchema(pkgPath, typeNameInfo.Name())
347+
ctx.requestSchemaWithPkg(pkgPath, typeNameInfo.Name(), pkg)
314348
link := TypeRefLink(pkgPath, typeNameInfo.Name())
315349

316350
// In cases where we have a named type, apply the type and format from the named schema
@@ -352,8 +386,9 @@ func namedToSchema(ctx *schemaContext, named *ast.SelectorExpr) *apiextensionsv1
352386
}
353387
typeInfo := typeInfoRaw.(interface{ Obj() *types.TypeName })
354388
typeNameInfo := typeInfo.Obj()
355-
nonVendorPath := loader.NonVendorPath(typeNameInfo.Pkg().Path())
356-
ctx.requestSchema(nonVendorPath, typeNameInfo.Name())
389+
typesPkg := typeNameInfo.Pkg()
390+
nonVendorPath := loader.NonVendorPath(typesPkg.Path())
391+
ctx.requestSchemaWithPkg(nonVendorPath, typeNameInfo.Name(), typesPkg)
357392
link := TypeRefLink(nonVendorPath, typeNameInfo.Name())
358393
return &apiextensionsv1.JSONSchemaProps{
359394
Ref: &link,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
Copyright 2026 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 typealias_base
18+
19+
type BaseStruct struct {
20+
Value string `json:"value"`
21+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
Copyright 2026 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+
// +groupName=testdata.kubebuilder.io
18+
// +versionName=v1
19+
package typealias_indirect
20+
21+
import "testdata.kubebuilder.io/cronjob/typealias_middle"
22+
23+
type IndirectAliasSpec struct {
24+
Field typealias_middle.AliasedStruct `json:"field"`
25+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
Copyright 2026 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 typealias_middle
18+
19+
import "testdata.kubebuilder.io/cronjob/typealias_base"
20+
21+
type AliasedStruct = typealias_base.BaseStruct

0 commit comments

Comments
 (0)