Skip to content

Commit d7f763b

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 3a2f680 commit d7f763b

File tree

2 files changed

+59
-34
lines changed

2 files changed

+59
-34
lines changed

cmd/cue/cmd/get_go.go

Lines changed: 54 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,18 @@ 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+
outer:
453+
for _, f := range p.Syntax {
454+
for _, c := range f.Comments {
455+
if strings.Contains(c.Text(), "+k8s:openapi-gen=true") {
456+
e.k8sSemantic = true
457+
break outer
458+
}
459+
}
460+
}
461+
e.logf("--- Package %s - Kubernetes semantics: %t", p.PkgPath, e.k8sSemantic)
450462

451463
e.recordTypeInfo(p)
452464

@@ -857,7 +869,7 @@ func (e *extractor) reportDecl(x *ast.GenDecl) (a []cueast.Decl) {
857869
if basic, ok := typ.(*types.Basic); ok && basic.Info()&types.IsUntyped != 0 {
858870
break // untyped basic types do not make valid identifiers
859871
}
860-
cv = cueast.NewBinExpr(cuetoken.AND, e.makeType(typ), cv)
872+
cv = cueast.NewBinExpr(cuetoken.AND, e.makeType(typ, regular), cv)
861873
}
862874

863875
f.Value = cv
@@ -1047,7 +1059,7 @@ const (
10471059
)
10481060

10491061
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)
1062+
typ := e.makeType(expr, kind)
10511063
var label cueast.Label
10521064
if kind == definition {
10531065
label = e.ident(name, true)
@@ -1075,7 +1087,7 @@ func (e *extractor) makeField(name string, kind fieldKind, expr types.Type, doc
10751087
return f, string(b)
10761088
}
10771089

1078-
func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
1090+
func (e *extractor) makeType(typ types.Type, kind fieldKind) (result cueast.Expr) {
10791091
switch typ := types.Unalias(typ).(type) {
10801092
case *types.Named:
10811093
obj := typ.Obj()
@@ -1209,10 +1221,14 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12091221

12101222
return result
12111223
case *types.Pointer:
1224+
// In K8s, a field is only nullable if explicitly marked as such.
1225+
if e.k8sSemantic && kind == optional {
1226+
return e.makeType(typ.Elem(), kind)
1227+
}
12121228
return &cueast.BinaryExpr{
12131229
X: cueast.NewNull(),
12141230
Op: cuetoken.OR,
1215-
Y: e.makeType(typ.Elem()),
1231+
Y: e.makeType(typ.Elem(), kind),
12161232
}
12171233

12181234
case *types.Struct:
@@ -1231,7 +1247,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12311247
if typ.Elem() == typeByte {
12321248
return e.ident("bytes", false)
12331249
}
1234-
return cueast.NewList(&cueast.Ellipsis{Type: e.makeType(typ.Elem())})
1250+
return cueast.NewList(&cueast.Ellipsis{Type: e.makeType(typ.Elem(), kind)})
12351251

12361252
case *types.Array:
12371253
if typ.Elem() == typeByte {
@@ -1248,7 +1264,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12481264
Value: strconv.Itoa(int(typ.Len())),
12491265
},
12501266
Op: cuetoken.MUL,
1251-
Y: cueast.NewList(e.makeType(typ.Elem())),
1267+
Y: cueast.NewList(e.makeType(typ.Elem(), kind)),
12521268
}
12531269
}
12541270

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

12601276
f := &cueast.Field{
12611277
Label: cueast.NewList(e.ident("string", false)),
1262-
Value: e.makeType(typ.Elem()),
1278+
Value: e.makeType(typ.Elem(), kind),
12631279
}
12641280
cueast.SetRelPos(f, cuetoken.Blank)
12651281
return &cueast.StructLit{
@@ -1282,7 +1298,7 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
12821298
case *types.Union:
12831299
var exprs []cueast.Expr
12841300
for term := range typ.Terms() {
1285-
exprs = append(exprs, e.makeType(term.Type()))
1301+
exprs = append(exprs, e.makeType(term.Type(), kind))
12861302
}
12871303
return cueast.NewBinExpr(cuetoken.OR, exprs...)
12881304

@@ -1305,12 +1321,12 @@ func (e *extractor) makeType(typ types.Type) (result cueast.Expr) {
13051321
//
13061322
var exprs []cueast.Expr
13071323
for etyp := range typ.EmbeddedTypes() {
1308-
exprs = append(exprs, e.makeType(etyp))
1324+
exprs = append(exprs, e.makeType(etyp, kind))
13091325
}
13101326
return cueast.NewBinExpr(cuetoken.OR, exprs...)
13111327

13121328
case *types.TypeParam:
1313-
return e.makeType(typ.Constraint())
1329+
return e.makeType(typ.Constraint(), kind)
13141330

13151331
default:
13161332
// record error
@@ -1360,7 +1376,7 @@ func (e *extractor) addFields(x *types.Struct, st *cueast.StructLit) {
13601376
}
13611377
switch typ := types.Unalias(typ).(type) {
13621378
case *types.Named:
1363-
embed := &cueast.EmbedDecl{Expr: e.makeType(typ)}
1379+
embed := &cueast.EmbedDecl{Expr: e.makeType(typ, regular)}
13641380
if i > 0 {
13651381
cueast.SetRelPos(embed, cuetoken.NewSection)
13661382
}
@@ -1381,10 +1397,7 @@ func (e *extractor) addFields(x *types.Struct, st *cueast.StructLit) {
13811397
doc := docs[i]
13821398

13831399
// TODO: check referrers
1384-
kind := regular
1385-
if e.isOptional(f, doc, tag) {
1386-
kind = optional
1387-
}
1400+
kind := e.detectFieldKind(f, doc, tag)
13881401
field, cueType := e.makeField(name, kind, f.Type(), doc, count > 0)
13891402
add(field)
13901403

@@ -1480,24 +1493,36 @@ func (e *extractor) isInline(tag string) bool {
14801493
hasFlag(tag, "yaml", "inline", 1)
14811494
}
14821495

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-
1496+
func (e *extractor) detectFieldKind(f *types.Var, doc *ast.CommentGroup, tag string) fieldKind {
1497+
// From k8s docs (https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md):
1498+
// - a map field marked with `+nullable` would accept either `foo: null` or `foo: {}`.
1499+
// - Using the `+optional` or the `omitempty` tag means that the field is optional.
1500+
// Note that for backward compatibility, any field that has the `omitempty` struct
1501+
// tag, and is not explicitly marked as `+required`, will be considered to be optional.
14881502
for line := range strings.SplitSeq(doc.Text(), "\n") {
14891503
before, _, _ := strings.Cut(strings.TrimSpace(line), "=")
1490-
if before == "+optional" {
1491-
return true
1504+
switch before {
1505+
case "+nullable":
1506+
return regular
1507+
case "+optional":
1508+
return optional
14921509
}
14931510
}
14941511

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

15031528
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
@@ -37,16 +37,16 @@ type Foo struct {
3737
package pkg1
3838

3939
#Foo: {
40-
required: string @go(Required)
41-
pointer?: null | string @go(Pointer,*string)
42-
omitEmptyPointer?: null | string @go(OmitEmptyPointer,*string)
40+
required: string @go(Required)
41+
pointer?: string @go(Pointer,*string)
42+
omitEmptyPointer?: string @go(OmitEmptyPointer,*string)
4343

4444
// +optional
4545
optionalComment?: string @go(OptionalComment)
4646

4747
// +optional
48-
optionalCommentPointer?: null | string @go(OptionalCommentPointer,*string)
48+
optionalCommentPointer?: string @go(OptionalCommentPointer,*string)
4949

5050
// +nullable
51-
nullableCommentPointer?: null | string @go(NullableCommentPointer,*string)
51+
nullableCommentPointer: null | string @go(NullableCommentPointer,*string)
5252
}

0 commit comments

Comments
 (0)