Skip to content

Commit da2c98e

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 da2c98e

File tree

2 files changed

+58
-34
lines changed

2 files changed

+58
-34
lines changed

cmd/cue/cmd/get_go.go

Lines changed: 53 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
@@ -1043,11 +1053,12 @@ type fieldKind int
10431053
const (
10441054
regular fieldKind = iota
10451055
optional
1056+
nullable
10461057
definition
10471058
)
10481059

10491060
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)
1061+
typ := e.makeType(expr, kind)
10511062
var label cueast.Label
10521063
if kind == definition {
10531064
label = e.ident(name, true)
@@ -1075,7 +1086,7 @@ func (e *extractor) makeField(name string, kind fieldKind, expr types.Type, doc
10751086
return f, string(b)
10761087
}
10771088

1078-
func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
1089+
func (e *extractor) makeType(typ types.Type, kind fieldKind) (result cueast.Expr) {
10791090
switch typ := types.Unalias(typ).(type) {
10801091
case *types.Named:
10811092
obj := typ.Obj()
@@ -1209,10 +1220,14 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12091220

12101221
return result
12111222
case *types.Pointer:
1223+
// In K8s, a field is only nullable if explicitly marked as such.
1224+
if e.k8sSemantic && kind == optional {
1225+
return e.makeType(typ.Elem(), kind)
1226+
}
12121227
return &cueast.BinaryExpr{
12131228
X: cueast.NewNull(),
12141229
Op: cuetoken.OR,
1215-
Y: e.makeType(typ.Elem()),
1230+
Y: e.makeType(typ.Elem(), kind),
12161231
}
12171232

12181233
case *types.Struct:
@@ -1231,7 +1246,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12311246
if typ.Elem() == typeByte {
12321247
return e.ident("bytes", false)
12331248
}
1234-
return cueast.NewList(&cueast.Ellipsis{Type: e.makeType(typ.Elem())})
1249+
return cueast.NewList(&cueast.Ellipsis{Type: e.makeType(typ.Elem(), kind)})
12351250

12361251
case *types.Array:
12371252
if typ.Elem() == typeByte {
@@ -1248,7 +1263,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12481263
Value: strconv.Itoa(int(typ.Len())),
12491264
},
12501265
Op: cuetoken.MUL,
1251-
Y: cueast.NewList(e.makeType(typ.Elem())),
1266+
Y: cueast.NewList(e.makeType(typ.Elem(), kind)),
12521267
}
12531268
}
12541269

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

12601275
f := &cueast.Field{
12611276
Label: cueast.NewList(e.ident("string", false)),
1262-
Value: e.makeType(typ.Elem()),
1277+
Value: e.makeType(typ.Elem(), kind),
12631278
}
12641279
cueast.SetRelPos(f, cuetoken.Blank)
12651280
return &cueast.StructLit{
@@ -1282,7 +1297,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12821297
case *types.Union:
12831298
var exprs []cueast.Expr
12841299
for term := range typ.Terms() {
1285-
exprs = append(exprs, e.makeType(term.Type()))
1300+
exprs = append(exprs, e.makeType(term.Type(), kind))
12861301
}
12871302
return cueast.NewBinExpr(cuetoken.OR, exprs...)
12881303

@@ -1305,12 +1320,12 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
13051320
//
13061321
var exprs []cueast.Expr
13071322
for etyp := range typ.EmbeddedTypes() {
1308-
exprs = append(exprs, e.makeType(etyp))
1323+
exprs = append(exprs, e.makeType(etyp, kind))
13091324
}
13101325
return cueast.NewBinExpr(cuetoken.OR, exprs...)
13111326

13121327
case *types.TypeParam:
1313-
return e.makeType(typ.Constraint())
1328+
return e.makeType(typ.Constraint(), kind)
13141329

13151330
default:
13161331
// record error
@@ -1360,7 +1375,7 @@ func (e *extractor) addFields(x *types.Struct, st *cueast.StructLit) {
13601375
}
13611376
switch typ := types.Unalias(typ).(type) {
13621377
case *types.Named:
1363-
embed := &cueast.EmbedDecl{Expr: e.makeType(typ)}
1378+
embed := &cueast.EmbedDecl{Expr: e.makeType(typ, regular)}
13641379
if i > 0 {
13651380
cueast.SetRelPos(embed, cuetoken.NewSection)
13661381
}
@@ -1381,10 +1396,7 @@ func (e *extractor) addFields(x *types.Struct, st *cueast.StructLit) {
13811396
doc := docs[i]
13821397

13831398
// TODO: check referrers
1384-
kind := regular
1385-
if e.isOptional(f, doc, tag) {
1386-
kind = optional
1387-
}
1399+
kind := e.detectFieldKind(f, doc, tag)
13881400
field, cueType := e.makeField(name, kind, f.Type(), doc, count > 0)
13891401
add(field)
13901402

@@ -1480,24 +1492,36 @@ func (e *extractor) isInline(tag string) bool {
14801492
hasFlag(tag, "yaml", "inline", 1)
14811493
}
14821494

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-
1495+
func (e *extractor) detectFieldKind(f *types.Var, doc *ast.CommentGroup, tag string) fieldKind {
1496+
// From k8s docs:
1497+
// - a map field marked with `+nullable` would accept either `foo: null` or `foo: {}`.
1498+
// - Using the `+optional` or the `omitempty` tag means that the field is optional.
1499+
// Note that for backward compatibility, any field that has the `omitempty` struct
1500+
// tag, and is not explicitly marked as `+required`, will be considered to be optional.
14881501
for line := range strings.SplitSeq(doc.Text(), "\n") {
14891502
before, _, _ := strings.Cut(strings.TrimSpace(line), "=")
1490-
if before == "+optional" {
1491-
return true
1503+
switch before {
1504+
case "+nullable":
1505+
return nullable
1506+
case "+optional":
1507+
return optional
14921508
}
14931509
}
14941510

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

15031527
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)