Kindsys: Target k8s style resource definitions (#67008)

Co-authored-by: sam boyer <sdboyer@grafana.com>
This commit is contained in:
Ryan McKinley
2023-04-27 23:32:38 +03:00
committed by GitHub
co-authored by sam boyer
parent b71b778d0d
commit ca1f79b9ba
80 changed files with 1991 additions and 640 deletions
+14 -6
View File
@@ -8,21 +8,25 @@ import (
"github.com/grafana/kindsys"
)
// LatestMajorsOrXJenny returns a jenny that repeats the input for the latest in each major version,
func LatestMajorsOrXJenny(parentdir string, inner codejen.OneToOne[SchemaForGen]) OneToMany {
// LatestMajorsOrXJenny returns a jenny that repeats the input for the latest in each major version.
//
// TODO remove forceGroup option, it's a temporary hack to accommodate core kinds
func LatestMajorsOrXJenny(parentdir string, forceGroup bool, inner codejen.OneToOne[SchemaForGen]) OneToMany {
if inner == nil {
panic("inner jenny must not be nil")
}
return &lmox{
parentdir: parentdir,
inner: inner,
parentdir: parentdir,
inner: inner,
forceGroup: forceGroup,
}
}
type lmox struct {
parentdir string
inner codejen.OneToOne[SchemaForGen]
parentdir string
inner codejen.OneToOne[SchemaForGen]
forceGroup bool
}
func (j *lmox) JennyName() string {
@@ -36,6 +40,10 @@ func (j *lmox) Generate(kind kindsys.Kind) (codejen.Files, error) {
IsGroup: comm.LineageIsGroup,
}
if j.forceGroup {
sfg.IsGroup = true
}
do := func(sfg SchemaForGen, infix string) (codejen.Files, error) {
f, err := j.inner.Generate(sfg)
if err != nil {
+118
View File
@@ -0,0 +1,118 @@
package codegen
import (
"bytes"
"fmt"
"strings"
"cuelang.org/go/cue"
"github.com/dave/dst/dstutil"
"github.com/grafana/codejen"
"github.com/grafana/kindsys"
"github.com/grafana/thema/encoding/gocode"
"github.com/grafana/thema/encoding/openapi"
)
type ResourceGoTypesJenny struct {
ApplyFuncs []dstutil.ApplyFunc
ExpandReferences bool
}
func (*ResourceGoTypesJenny) JennyName() string {
return "GoTypesJenny"
}
func (ag *ResourceGoTypesJenny) Generate(kind kindsys.Kind) (*codejen.File, error) {
comm := kind.Props().Common()
sfg := SchemaForGen{
Name: comm.Name,
Schema: kind.Lineage().Latest(),
IsGroup: comm.LineageIsGroup,
}
sch := sfg.Schema
iter, err := sch.Underlying().Fields()
if err != nil {
return nil, err
}
var subr []string
for iter.Next() {
subr = append(subr, typeNameFromKey(iter.Selector().String()))
}
buf := new(bytes.Buffer)
mname := kind.Props().Common().MachineName
if err := tmpls.Lookup("core_resource.tmpl").Execute(buf, tvars_resource{
PackageName: mname,
KindName: kind.Props().Common().Name,
SubresourceNames: subr,
}); err != nil {
return nil, fmt.Errorf("failed executing core resource template: %w", err)
}
if err != nil {
return nil, err
}
return codejen.NewFile(fmt.Sprintf("pkg/kinds/%s/%s_gen.go", mname, mname), buf.Bytes(), ag), nil
}
type SubresourceGoTypesJenny struct {
ApplyFuncs []dstutil.ApplyFunc
ExpandReferences bool
}
func (*SubresourceGoTypesJenny) JennyName() string {
return "GoResourceTypes"
}
func (g *SubresourceGoTypesJenny) Generate(kind kindsys.Kind) (codejen.Files, error) {
comm := kind.Props().Common()
sfg := SchemaForGen{
Name: comm.Name,
Schema: kind.Lineage().Latest(),
IsGroup: comm.LineageIsGroup,
}
sch := sfg.Schema
// Iterate through all top-level fields and make go types for them
// (this should consist of "spec" and arbitrary subresources)
i, err := sch.Underlying().Fields()
if err != nil {
return nil, err
}
files := make(codejen.Files, 0)
for i.Next() {
str := i.Selector().String()
b, err := gocode.GenerateTypesOpenAPI(sch, &gocode.TypeConfigOpenAPI{
// TODO will need to account for sanitizing e.g. dashes here at some point
Config: &openapi.Config{
Group: false, // TODO: better
RootName: typeNameFromKey(str),
Subpath: cue.MakePath(cue.Str(str)),
},
PackageName: sfg.Schema.Lineage().Name(),
ApplyFuncs: append(g.ApplyFuncs, PrefixDropper(sfg.Name)),
})
if err != nil {
return nil, err
}
name := sfg.Schema.Lineage().Name()
files = append(files, codejen.File{
RelativePath: fmt.Sprintf("pkg/kinds/%s/%s_%s_gen.go", name, name, strings.ToLower(str)),
Data: b,
From: []codejen.NamedJenny{g},
})
}
return files, nil
}
func typeNameFromKey(key string) string {
if len(key) > 0 {
return strings.ToUpper(key[:1]) + key[1:]
}
return strings.ToUpper(key)
}
+79
View File
@@ -0,0 +1,79 @@
package codegen
import (
"github.com/grafana/codejen"
"github.com/grafana/cuetsy/ts"
"github.com/grafana/cuetsy/ts/ast"
"github.com/grafana/thema/encoding/typescript"
)
// TSResourceJenny is a [OneToOne] that produces TypeScript types and
// defaults for a Thema schema.
//
// Thema's generic TS jenny will be able to replace this one once
// https://github.com/grafana/thema/issues/89 is complete.
type TSResourceJenny struct{}
var _ codejen.OneToOne[SchemaForGen] = &TSResourceJenny{}
func (j TSResourceJenny) JennyName() string {
return "TSResourceJenny"
}
func (j TSResourceJenny) Generate(sfg SchemaForGen) (*codejen.File, error) {
// TODO allow using name instead of machine name in thema generator
f, err := typescript.GenerateTypes(sfg.Schema, &typescript.TypeConfig{
RootName: sfg.Name,
Group: sfg.IsGroup,
})
if err != nil {
return nil, err
}
renameSpecNode(sfg.Name, f)
return codejen.NewFile(sfg.Schema.Lineage().Name()+"_types.gen.ts", []byte(f.String()), j), nil
}
func renameSpecNode(name string, tf *ast.File) {
specidx, specdefidx := -1, -1
for idx, def := range tf.Nodes {
// Peer through export keywords
if ex, is := def.(ast.ExportKeyword); is {
def = ex.Decl
}
switch x := def.(type) {
case ast.TypeDecl:
if x.Name.Name == "spec" {
specidx = idx
x.Name.Name = name
tf.Nodes[idx] = x
}
case ast.VarDecl:
// Before:
// export const defaultspec: Partial<spec> = {
// After:
/// export const defaultPlaylist: Partial<Playlist> = {
if x.Names.Idents[0].Name == "defaultspec" {
specdefidx = idx
x.Names.Idents[0].Name = "default" + name
tt := x.Type.(ast.TypeTransformExpr)
tt.Expr = ts.Ident(name)
x.Type = tt
tf.Nodes[idx] = x
}
}
}
if specidx != -1 {
decl := tf.Nodes[specidx]
tf.Nodes = append(append(tf.Nodes[:specidx], tf.Nodes[specidx+1:]...), decl)
}
if specdefidx != -1 {
if specdefidx > specidx {
specdefidx--
}
decl := tf.Nodes[specdefidx]
tf.Nodes = append(append(tf.Nodes[:specdefidx], tf.Nodes[specdefidx+1:]...), decl)
}
}
+8 -3
View File
@@ -50,6 +50,7 @@ func (gen *genTSVeneerIndex) Generate(kinds ...kindsys.Kind) (*codejen.File, err
if err != nil {
return nil, fmt.Errorf("%s: %w", def.Props().Common().Name, err)
}
renameSpecNode(def.Props().Common().Name, f)
elems, err := gen.extractTSIndexVeneerElements(def, f)
if err != nil {
return nil, fmt.Errorf("%s: %w", def.Props().Common().Name, err)
@@ -75,13 +76,13 @@ func (gen *genTSVeneerIndex) extractTSIndexVeneerElements(def kindsys.Kind, tf *
sels := p.Selectors()
switch len(sels) {
case 0:
name = comm.Name
fallthrough
return true
case 1:
// Only deal with subpaths that are definitions, for now
// TODO incorporate smarts about grouped lineages here
if name == "" {
if !sels[0].IsDefinition() {
if !(sels[0].IsDefinition() || sels[0].String() == "spec") {
return false
}
// It might seem to make sense that we'd strip replaceout the leading # here for
@@ -90,6 +91,10 @@ func (gen *genTSVeneerIndex) extractTSIndexVeneerElements(def kindsys.Kind, tf *
name = sels[0].String()
}
if name == "spec" {
name = comm.Name
}
// Search the generated TS AST for the type and default def nodes
pair := findDeclNode(name, tf)
if pair.T == nil {
+8 -1
View File
@@ -3,6 +3,7 @@ package codegen
import (
"bytes"
"embed"
"strings"
"text/template"
"time"
@@ -15,7 +16,8 @@ var tmpls *template.Template
func init() {
base := template.New("codegen").Funcs(template.FuncMap{
"now": time.Now,
"now": time.Now,
"ToLower": strings.ToLower,
})
tmpls = template.Must(base.ParseFS(tmplFS, "tmpl/*.tmpl"))
}
@@ -46,6 +48,11 @@ type (
tvars_coremodel_imports struct {
PackageName string
}
tvars_resource struct {
PackageName string
KindName string
SubresourceNames []string
}
)
type HeaderVars = tvars_autogen_header
+7
View File
@@ -0,0 +1,7 @@
package {{ .PackageName }}
// Resource is the wire representation of {{ .KindName }}. (TODO be better)
type Resource struct {
{{- range .SubresourceNames }}
{{ . }} {{ . }} `json:"{{ . | ToLower }}"`{{end}}
}
+7 -7
View File
@@ -17,9 +17,9 @@ const rootrel string = "kinds/{{ .Props.MachineName }}"
// TODO standard generated docs
type Kind struct {
kindsys.Core
lin thema.ConvergentLineage[*{{ .Props.Name }}]
lin thema.ConvergentLineage[*Resource]
jcodec vmux.Codec
valmux vmux.ValueMux[*{{ .Props.Name }}]
valmux vmux.ValueMux[*Resource]
}
// type guard - ensure generated Kind type satisfies the kindsys.Core interface
@@ -40,7 +40,7 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
// Get the thema.Schema that the meta says is in the current version (which
// codegen ensures is always the latest)
cursch := thema.SchemaP(k.Core.Lineage(), def.Properties.CurrentVersion)
tsch, err := thema.BindType[*{{ .Props.Name }}](cursch, &{{ .Props.Name }}{})
tsch, err := thema.BindType(cursch, &Resource{})
if err != nil {
// Should be unreachable, modulo bugs in the Thema->Go code generator
return nil, err
@@ -53,18 +53,18 @@ func NewKind(rt *thema.Runtime, opts ...thema.BindOption) (*Kind, error) {
}
// ConvergentLineage returns the same [thema.Lineage] as Lineage, but bound (see [thema.BindType])
// to the the {{ .Props.Name }} type generated from the current schema, v{{ .Props.CurrentVersion }}.
func (k *Kind) ConvergentLineage() thema.ConvergentLineage[*{{ .Props.Name }}] {
// to the the {{ .Props.Name }} [Resource] type generated from the current schema, v{{ .Props.CurrentVersion }}.
func (k *Kind) ConvergentLineage() thema.ConvergentLineage[*Resource] {
return k.lin
}
// JSONValueMux is a version multiplexer that maps a []byte containing JSON data
// at any schematized dashboard version to an instance of {{ .Props.Name }}.
// at any schematized dashboard version to an instance of {{ .Props.Name }} [Resource].
//
// Validation and translation errors emitted from this func will identify the
// input bytes as "dashboard.json".
//
// This is a thin wrapper around Thema's [vmux.ValueMux].
func (k *Kind) JSONValueMux(b []byte) (*{{ .Props.Name }}, thema.TranslationLacunas, error) {
func (k *Kind) JSONValueMux(b []byte) (*Resource, thema.TranslationLacunas, error) {
return k.valmux(b)
}