Skip to content
Closed
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
31 changes: 31 additions & 0 deletions internal/transpiler/support.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,34 @@ func contains(s []string, e string) bool {
}
return false
}

// reservedTypeNames are Go identifiers that crdgen emits as package-level
// declarations (constants, vars, funcs) in the generated apis package,
// alongside the struct types produced from the JSON schema. A generated
// struct type must never reuse one of these names or it would redeclare /
// shadow the package-level identifier and break code generation.
//
// The canonical failure is a schema whose top-level property is named
// "group" (e.g. the OpenStack Keystone group envelope): it yields
// `type Group struct{...}` which collides with `const Group = "<api group>"`
// emitted in groupversion_info.go.
var reservedTypeNames = map[string]struct{}{
"Group": {},
"Version": {},
"SchemeGroupVersion": {},
"SchemeBuilder": {},
"AddToScheme": {},
"AddToSchemes": {},
}

// safeTypeName returns a Go type name that does not collide with the
// package-level identifiers emitted by crdgen. Colliding names are given an
// "Envelope" suffix (e.g. "Group" -> "GroupEnvelope"), which is stable and
// idempotent and avoids clashing with the <Kind>Spec/<Kind>Status/<Kind>List
// types that crdgen also generates.
func safeTypeName(name string) string {
if _, reserved := reservedTypeNames[name]; reserved {
return name + "Envelope"
}
return name
}
11 changes: 11 additions & 0 deletions internal/transpiler/transpiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,17 @@ func (g *transpiler) processArray(name string, schema *jsonschema.Schema) (strin
// schema: detail incl properties & child objects
// returns: generated type
func (g *transpiler) processObject(name string, schema *jsonschema.Schema) (typ string, err error) {
// A generated struct type lives in the same Go package as the
// package-level identifiers emitted by crdgen (e.g. the `Group` and
// `Version` constants in groupversion_info.go). If a schema property is
// named after one of those identifiers (the OpenStack Keystone `group`
// envelope is the canonical example) the resulting `type Group struct`
// collides with `const Group`, producing an "unknown type Group" error
// during CRD generation. Rename the *type* to a non-colliding name; the
// struct field keeps its original name/JSON tag because that is handled
// separately by the caller.
name = safeTypeName(name)

strct := Struct{
Name: name,
Description: schema.Description,
Expand Down
84 changes: 84 additions & 0 deletions internal/transpiler/transpiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -647,3 +647,87 @@ func contains(s []string, e string) bool {
}
return false
}

// TestReservedTypeNameCollision is a regression test for the bug where an
// OpenAPI/JSON schema property named after an identifier that crdgen emits at
// package scope (most notably `group`, which collides with the `Group`
// constant generated in groupversion_info.go) produced an undefined Go type
// and broke CRD generation with "unknown type Group".
//
// The generated struct type for such a property must be renamed so it no
// longer collides, while the field name and JSON tag keep the original name.
func TestReservedTypeNameCollision(t *testing.T) {
cases := []struct {
envelope string // JSON property name (also the naive type name)
goName string // expected Go field name (unchanged)
wantRename bool // whether the generated type must be renamed
}{
{"group", "Group", true},
{"version", "Version", true},
{"project", "Project", false},
{"domain", "Domain", false},
{"user", "User", false},
{"role", "Role", false},
}

for _, tc := range cases {
t.Run(tc.envelope, func(t *testing.T) {
root := jsonschema.Schema{
SchemaType: "http://json-schema.org/draft-04/schema#",
TypeValue: "object",
Required: []string{tc.envelope},
Properties: map[string]*jsonschema.Schema{
tc.envelope: {
TypeValue: "object",
Required: []string{"name"},
Properties: map[string]*jsonschema.Schema{
"name": {TypeValue: "string"},
"domain_id": {TypeValue: "string"},
},
},
},
}
root.Init()

structs, err := transpiler.Transpile(&root)
if err != nil {
t.Fatalf("transpile error: %v", err)
}

rootStruct, ok := structs["Root"]
if !ok {
t.Fatal("Root struct not found")
}

field, ok := rootStruct.Fields[tc.goName]
if !ok {
t.Fatalf("field %q not found; got %v", tc.goName, rootStruct.Fields)
}
if field.JSONName != tc.envelope {
t.Errorf("JSON name = %q, want %q", field.JSONName, tc.envelope)
}

// The field type must reference a struct that is actually defined.
bare := strings.TrimPrefix(field.Type, "*")
if _, defined := structs[bare]; !defined {
t.Fatalf("field type %q references an undefined struct; defined: %v", field.Type, structKeys(structs))
}

// Colliding names must be renamed away from the reserved identifier.
if tc.wantRename && bare == tc.goName {
t.Errorf("type %q was not renamed and collides with a reserved package-level identifier", bare)
}
if !tc.wantRename && bare != tc.goName {
t.Errorf("type = %q, want %q (should not be renamed)", bare, tc.goName)
}
})
}
}

func structKeys(m map[string]transpiler.Struct) []string {
out := make([]string, 0, len(m))
for k := range m {
out = append(out, k)
}
return out
}
Loading