Skip to content

Commit 289db25

Browse files
committed
cmd/cue: detect kubernetes semantics for get go
Kubernetes types with either +optional or omitempty are expected to not have null disjunctions. Fields with +nullable are required and have a valid null value. By checking for the k8s openapi annotations we can switch to a kubernetes semantic mode and generate the schema correctly. Change-Id: I747de2ddda57f8bc079afe9180a8179fd5b2c778 Signed-off-by: Tim Windelschmidt <[email protected]>
1 parent 12e5493 commit 289db25

File tree

2 files changed

+57
-34
lines changed

2 files changed

+57
-34
lines changed

cmd/cue/cmd/get_go.go

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -254,10 +254,11 @@ type extractor struct {
254254
done map[string]bool
255255

256256
// per package
257-
pkg *packages.Package
258-
orig map[types.Type]*ast.StructType
259-
usedPkgs map[string]bool
260-
consts map[string][]string
257+
pkg *packages.Package
258+
orig map[types.Type]*ast.StructType
259+
usedPkgs map[string]bool
260+
consts map[string][]string
261+
k8sSemantic bool
261262

262263
// per file
263264
cmap ast.CommentMap
@@ -446,7 +447,16 @@ func (e *extractor) recordTypeInfo(p *packages.Package) {
446447

447448
func (e *extractor) extractPkg(root string, p *packages.Package) error {
448449
e.pkg = p
449-
e.logf("--- Package %s", p.PkgPath)
450+
e.k8sSemantic = false
451+
452+
for _, f := range p.Syntax {
453+
for _, c := range f.Comments {
454+
if strings.Contains(c.Text(), "+k8s:openapi-gen=true") {
455+
e.k8sSemantic = true
456+
}
457+
}
458+
}
459+
e.logf("--- Package %s - Kubernetes semantics: %t", p.PkgPath, e.k8sSemantic)
450460

451461
e.recordTypeInfo(p)
452462

@@ -857,7 +867,7 @@ func (e *extractor) reportDecl(x *ast.GenDecl) (a []cueast.Decl) {
857867
if basic, ok := typ.(*types.Basic); ok && basic.Info()&types.IsUntyped != 0 {
858868
break // untyped basic types do not make valid identifiers
859869
}
860-
cv = cueast.NewBinExpr(cuetoken.AND, e.makeType(typ), cv)
870+
cv = cueast.NewBinExpr(cuetoken.AND, e.makeType(typ, regular), cv)
861871
}
862872

863873
f.Value = cv
@@ -1047,7 +1057,7 @@ const (
10471057
)
10481058

10491059
func (e *extractor) makeField(name string, kind fieldKind, expr types.Type, doc *ast.CommentGroup, newline bool) (f *cueast.Field, typename string) {
1050-
typ := e.makeType(expr)
1060+
typ := e.makeType(expr, kind)
10511061
var label cueast.Label
10521062
if kind == definition {
10531063
label = e.ident(name, true)
@@ -1075,7 +1085,7 @@ func (e *extractor) makeField(name string, kind fieldKind, expr types.Type, doc
10751085
return f, string(b)
10761086
}
10771087

1078-
func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
1088+
func (e *extractor) makeType(typ types.Type, kind fieldKind) (result cueast.Expr) {
10791089
switch typ := types.Unalias(typ).(type) {
10801090
case *types.Named:
10811091
obj := typ.Obj()
@@ -1209,10 +1219,14 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12091219

12101220
return result
12111221
case *types.Pointer:
1222+
// In K8s, a field is only nullable if explicitly marked as such.
1223+
if e.k8sSemantic && kind == optional {
1224+
return e.makeType(typ.Elem(), kind)
1225+
}
12121226
return &cueast.BinaryExpr{
12131227
X: cueast.NewNull(),
12141228
Op: cuetoken.OR,
1215-
Y: e.makeType(typ.Elem()),
1229+
Y: e.makeType(typ.Elem(), kind),
12161230
}
12171231

12181232
case *types.Struct:
@@ -1231,7 +1245,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12311245
if typ.Elem() == typeByte {
12321246
return e.ident("bytes", false)
12331247
}
1234-
return cueast.NewList(&cueast.Ellipsis{Type: e.makeType(typ.Elem())})
1248+
return cueast.NewList(&cueast.Ellipsis{Type: e.makeType(typ.Elem(), kind)})
12351249

12361250
case *types.Array:
12371251
if typ.Elem() == typeByte {
@@ -1248,7 +1262,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12481262
Value: strconv.Itoa(int(typ.Len())),
12491263
},
12501264
Op: cuetoken.MUL,
1251-
Y: cueast.NewList(e.makeType(typ.Elem())),
1265+
Y: cueast.NewList(e.makeType(typ.Elem(), kind)),
12521266
}
12531267
}
12541268

@@ -1259,7 +1273,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12591273

12601274
f := &cueast.Field{
12611275
Label: cueast.NewList(e.ident("string", false)),
1262-
Value: e.makeType(typ.Elem()),
1276+
Value: e.makeType(typ.Elem(), kind),
12631277
}
12641278
cueast.SetRelPos(f, cuetoken.Blank)
12651279
return &cueast.StructLit{
@@ -1282,7 +1296,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12821296
case *types.Union:
12831297
var exprs []cueast.Expr
12841298
for term := range typ.Terms() {
1285-
exprs = append(exprs, e.makeType(term.Type()))
1299+
exprs = append(exprs, e.makeType(term.Type(), kind))
12861300
}
12871301
return cueast.NewBinExpr(cuetoken.OR, exprs...)
12881302

@@ -1305,12 +1319,12 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
13051319
//
13061320
var exprs []cueast.Expr
13071321
for etyp := range typ.EmbeddedTypes() {
1308-
exprs = append(exprs, e.makeType(etyp))
1322+
exprs = append(exprs, e.makeType(etyp, kind))
13091323
}
13101324
return cueast.NewBinExpr(cuetoken.OR, exprs...)
13111325

13121326
case *types.TypeParam:
1313-
return e.makeType(typ.Constraint())
1327+
return e.makeType(typ.Constraint(), kind)
13141328

13151329
default:
13161330
// record error
@@ -1360,7 +1374,7 @@ func (e *extractor) addFields(x *types.Struct, st *cueast.StructLit) {
13601374
}
13611375
switch typ := types.Unalias(typ).(type) {
13621376
case *types.Named:
1363-
embed := &cueast.EmbedDecl{Expr: e.makeType(typ)}
1377+
embed := &cueast.EmbedDecl{Expr: e.makeType(typ, regular)}
13641378
if i > 0 {
13651379
cueast.SetRelPos(embed, cuetoken.NewSection)
13661380
}
@@ -1381,10 +1395,7 @@ func (e *extractor) addFields(x *types.Struct, st *cueast.StructLit) {
13811395
doc := docs[i]
13821396

13831397
// TODO: check referrers
1384-
kind := regular
1385-
if e.isOptional(f, doc, tag) {
1386-
kind = optional
1387-
}
1398+
kind := e.detectFieldKind(f, doc, tag)
13881399
field, cueType := e.makeField(name, kind, f.Type(), doc, count > 0)
13891400
add(field)
13901401

@@ -1480,24 +1491,36 @@ func (e *extractor) isInline(tag string) bool {
14801491
hasFlag(tag, "yaml", "inline", 1)
14811492
}
14821493

1483-
func (e *extractor) isOptional(f *types.Var, doc *ast.CommentGroup, tag string) bool {
1484-
if _, ok := f.Type().(*types.Pointer); ok {
1485-
return true
1486-
}
1487-
1494+
func (e *extractor) detectFieldKind(f *types.Var, doc *ast.CommentGroup, tag string) fieldKind {
1495+
// From k8s docs:
1496+
// - a map field marked with `+nullable` would accept either `foo: null` or `foo: {}`.
1497+
// - Using the `+optional` or the `omitempty` tag means that the field is optional.
1498+
// Note that for backward compatibility, any field that has the `omitempty` struct
1499+
// tag, and is not explicitly marked as `+required`, will be considered to be optional.
14881500
for line := range strings.SplitSeq(doc.Text(), "\n") {
14891501
before, _, _ := strings.Cut(strings.TrimSpace(line), "=")
1490-
if before == "+optional" {
1491-
return true
1502+
switch before {
1503+
case "+nullable":
1504+
return regular
1505+
case "+optional":
1506+
return optional
14921507
}
14931508
}
14941509

1510+
if _, ok := f.Type().(*types.Pointer); ok {
1511+
return optional
1512+
}
1513+
14951514
// Go 1.24 added the "omitzero" option to encoding/json, an improvement over "omitempty".
14961515
// Note that, as of mid 2025, YAML libraries don't seem to have picked up "omitzero" yet.
14971516
// TODO: also when the type is a list or other kind of pointer.
1498-
return hasFlag(tag, "json", "omitempty", 1) ||
1517+
if hasFlag(tag, "json", "omitempty", 1) ||
14991518
hasFlag(tag, "json", "omitzero", 1) ||
1500-
hasFlag(tag, "yaml", "omitempty", 1)
1519+
hasFlag(tag, "yaml", "omitempty", 1) {
1520+
return optional
1521+
}
1522+
1523+
return regular
15011524
}
15021525

15031526
func hasFlag(tag, key, flag string, offset int) bool {

cmd/cue/cmd/testdata/script/get_go_types_kubernetes.txtar

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,13 @@ type Foo struct {
3636
package pkg1
3737

3838
#Foo: {
39-
required: string @go(Required)
40-
pointer?: null | string @go(Pointer,*string)
41-
omitEmptyTagPointer?: null | string @go(OmitEmptyTagPointer,*string)
39+
required: string @go(Required)
40+
pointer?: string @go(Pointer,*string)
41+
omitEmptyTagPointer?: string @go(OmitEmptyTagPointer,*string)
4242

4343
// +optional
44-
optionalCommentTagPointer?: null | string @go(OptionalCommentTagPointer,*string)
44+
optionalCommentTagPointer?: string @go(OptionalCommentTagPointer,*string)
4545

4646
// +nullable
47-
nullableCommentTagPointer?: null | string @go(NullableCommentTagPointer,*string)
47+
nullableCommentTagPointer: null | string @go(NullableCommentTagPointer,*string)
4848
}

0 commit comments

Comments
 (0)