Schemas: Generate CRDs for core kinds (#62641)

Co-authored-by: sam boyer <sdboyer@grafana.com>
This commit is contained in:
Ryan McKinley
2023-02-01 09:08:26 -08:00
committed by GitHub
co-authored by sam boyer
parent b95eda045a
commit e70d623f90
34 changed files with 1748 additions and 34 deletions
+16 -10
View File
@@ -34,21 +34,27 @@ func ForLatestSchema(k kindsys.Kind) SchemaForGen {
// file.
func SlashHeaderMapper(maingen string) codejen.FileMapper {
return func(f codejen.File) (codejen.File, error) {
var leader string
// Never inject on certain filetypes, it's never valid
switch filepath.Ext(f.RelativePath) {
case ".json", ".yml", ".yaml", ".md":
case ".json", ".md":
return f, nil
case ".yml", ".yaml":
leader = "#"
default:
buf := new(bytes.Buffer)
if err := tmpls.Lookup("gen_header.tmpl").Execute(buf, tvars_gen_header{
MainGenerator: maingen,
Using: f.From,
}); err != nil {
return codejen.File{}, fmt.Errorf("failed executing gen header template: %w", err)
}
fmt.Fprint(buf, string(f.Data))
f.Data = buf.Bytes()
leader = "//"
}
buf := new(bytes.Buffer)
if err := tmpls.Lookup("gen_header.tmpl").Execute(buf, tvars_gen_header{
MainGenerator: maingen,
Using: f.From,
Leader: leader,
}); err != nil {
return codejen.File{}, fmt.Errorf("failed executing gen header template: %w", err)
}
fmt.Fprint(buf, string(f.Data))
f.Data = buf.Bytes()
return f, nil
}
}
+11 -1
View File
@@ -33,11 +33,21 @@ func (gen *genBaseRegistry) JennyName() string {
}
func (gen *genBaseRegistry) Generate(kinds ...kindsys.Kind) (*codejen.File, error) {
cores := make([]kindsys.Core, 0, len(kinds))
for _, d := range kinds {
if corekind, is := d.(kindsys.Core); is {
cores = append(cores, corekind)
}
}
if len(cores) == 0 {
return nil, nil
}
buf := new(bytes.Buffer)
if err := tmpls.Lookup("kind_registry.tmpl").Execute(buf, tvars_kind_registry{
PackageName: filepath.Base(gen.path),
KindPackagePrefix: filepath.ToSlash(filepath.Join("github.com/grafana/grafana", gen.kindrelroot)),
Kinds: kinds,
Kinds: cores,
}); err != nil {
return nil, fmt.Errorf("failed executing kind registry template: %w", err)
}
+61
View File
@@ -0,0 +1,61 @@
package codegen
import (
"bytes"
"fmt"
"path/filepath"
"github.com/grafana/codejen"
"github.com/grafana/grafana/pkg/kindsys"
)
// CRDKindRegistryJenny generates a static registry of the CRD representations
// of core Grafana kinds, layered on top of the publicly consumable generated
// registry in pkg/corekinds.
//
// Path should be the relative path to the directory that will contain the
// generated registry.
func CRDKindRegistryJenny(path string) ManyToOne {
return &crdregjenny{
path: path,
}
}
type crdregjenny struct {
path string
}
func (j *crdregjenny) JennyName() string {
return "CRDKindRegistryJenny"
}
func (j *crdregjenny) Generate(kinds ...kindsys.Kind) (*codejen.File, error) {
cores := make([]kindsys.Core, 0, len(kinds))
for _, d := range kinds {
if corekind, is := d.(kindsys.Core); is {
cores = append(cores, corekind)
}
}
if len(cores) == 0 {
return nil, nil
}
buf := new(bytes.Buffer)
if err := tmpls.Lookup("core_crd_registry.tmpl").Execute(buf, tvars_kind_registry{
PackageName: "corecrd",
KindPackagePrefix: filepath.ToSlash(filepath.Join("github.com/grafana/grafana", kindsys.GoCoreKindParentPath)),
Kinds: cores,
}); err != nil {
return nil, fmt.Errorf("failed executing core crd registry template: %w", err)
}
b, err := postprocessGoFile(genGoFile{
path: j.path,
in: buf.Bytes(),
})
if err != nil {
return nil, err
}
return codejen.NewFile(filepath.Join(j.path, "registry_gen.go"), b, j), nil
}
+51
View File
@@ -0,0 +1,51 @@
package codegen
import (
"bytes"
"fmt"
"path/filepath"
"github.com/grafana/codejen"
"github.com/grafana/grafana/pkg/kindsys"
)
// CRDTypesJenny generates the OpenAPI CRD representation for a core
// structured kind that is expected by Kubernetes controller machinery.
func CRDTypesJenny(path string) OneToOne {
return crdTypesJenny{
parentpath: path,
}
}
type crdTypesJenny struct {
parentpath string
}
func (j crdTypesJenny) JennyName() string {
return "CRDTypesJenny"
}
func (j crdTypesJenny) Generate(kind kindsys.Kind) (*codejen.File, error) {
_, isCore := kind.(kindsys.Core)
_, isCustom := kind.(kindsys.Core)
if !(isCore || isCustom) {
return nil, nil
}
buf := new(bytes.Buffer)
if err := tmpls.Lookup("core_crd_types.tmpl").Execute(buf, kind); err != nil {
return nil, fmt.Errorf("failed executing crd types template: %w", err)
}
name := kind.Props().Common().MachineName
path := filepath.Join(j.parentpath, name, "crd", name+"_crd_gen.go")
b, err := postprocessGoFile(genGoFile{
path: path,
in: buf.Bytes(),
})
if err != nil {
return nil, err
}
return codejen.NewFile(path, b, j), nil
}
+214
View File
@@ -0,0 +1,214 @@
package codegen
import (
"bytes"
"fmt"
"path/filepath"
"cuelang.org/go/cue"
"cuelang.org/go/cue/ast"
"cuelang.org/go/encoding/openapi"
cueyaml "cuelang.org/go/pkg/encoding/yaml"
"github.com/grafana/codejen"
"github.com/grafana/grafana/pkg/kindsys"
"github.com/grafana/grafana/pkg/kindsys/k8ssys"
"github.com/grafana/thema"
goyaml "gopkg.in/yaml.v3"
)
// TODO this jenny is quite sloppy, having been quickly adapted from app-sdk. It needs love
// YamlCRDJenny generates a representation of a core structured kind in YAML CRD form.
func YamlCRDJenny(path string) OneToOne {
return yamlCRDJenny{
parentpath: path,
}
}
type yamlCRDJenny struct {
parentpath string
}
func (yamlCRDJenny) JennyName() string {
return "YamlCRDJenny"
}
func (j yamlCRDJenny) Generate(k kindsys.Kind) (*codejen.File, error) {
kind, is := k.(kindsys.Core)
if !is {
return nil, nil
}
props := kind.Def().Properties
lin := kind.Lineage()
// We need to go through every schema, as they all have to be defined in the CRD
sch, err := lin.Schema(thema.SV(0, 0))
if err != nil {
return nil, err
}
resource := customResourceDefinition{
APIVersion: "apiextensions.k8s.io/v1",
Kind: "CustomResourceDefinition",
Metadata: customResourceDefinitionMetadata{
Name: fmt.Sprintf("%s.%s", props.PluralMachineName, props.CRD.Group),
},
Spec: k8ssys.CustomResourceDefinitionSpec{
Group: props.CRD.Group,
Scope: props.CRD.Scope,
Names: k8ssys.CustomResourceDefinitionSpecNames{
Kind: props.Name,
Plural: props.PluralMachineName,
},
Versions: make([]k8ssys.CustomResourceDefinitionSpecVersion, 0),
},
}
latest := lin.Latest().Version()
for sch != nil {
oapi, err := generateOpenAPI(sch, props)
if err != nil {
return nil, err
}
vstr := versionString(sch.Version())
if props.Maturity.Less(kindsys.MaturityStable) {
vstr = "v0-0alpha1"
}
ver, err := valueToCRDSpecVersion(oapi, vstr, sch.Version() == latest)
if err != nil {
return nil, err
}
if props.CRD.DummySchema {
ver.Schema = map[string]any{
"openAPIV3Schema": map[string]any{
"type": "object",
"properties": map[string]any{
"spec": map[string]any{
"type": "object",
"x-kubernetes-preserve-unknown-fields": true,
},
},
"required": []any{
"spec",
},
},
}
}
resource.Spec.Versions = append(resource.Spec.Versions, ver)
sch = sch.Successor()
}
contents, err := goyaml.Marshal(resource)
if err != nil {
return nil, err
}
if props.CRD.DummySchema {
// Add a comment header for those with dummy schema
b := new(bytes.Buffer)
fmt.Fprintf(b, "# This CRD is generated with an empty schema body because Grafana's\n# code generators currently produce OpenAPI that Kubernetes will not\n# accept, despite being valid.\n\n%s", string(contents))
contents = b.Bytes()
}
return codejen.NewFile(filepath.Join(j.parentpath, props.MachineName, "crd", props.MachineName+".crd.yml"), contents, j), nil
}
// customResourceDefinition differs from k8ssys.CustomResourceDefinition in that it doesn't use the metav1
// TypeMeta and ObjectMeta, as those do not contain YAML tags and get improperly serialized to YAML.
// Since we don't need to use it with the kubernetes go-client, we don't need the extra functionality attached.
//
//nolint:lll
type customResourceDefinition struct {
Kind string `json:"kind,omitempty" yaml:"kind,omitempty" protobuf:"bytes,1,opt,name=kind"`
APIVersion string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty" protobuf:"bytes,2,opt,name=apiVersion"`
Metadata customResourceDefinitionMetadata `json:"metadata,omitempty" yaml:"metadata,omitempty"`
Spec k8ssys.CustomResourceDefinitionSpec `json:"spec"`
}
type customResourceDefinitionMetadata struct {
Name string `json:"name,omitempty" yaml:"name" protobuf:"bytes,1,opt,name=name"`
// TODO: other fields as necessary for codegen
}
type cueOpenAPIEncoded struct {
Components cueOpenAPIEncodedComponents `json:"components"`
}
type cueOpenAPIEncodedComponents struct {
Schemas map[string]any `json:"schemas"`
}
func valueToCRDSpecVersion(str string, name string, stored bool) (k8ssys.CustomResourceDefinitionSpecVersion, error) {
// Decode the bytes back into an object where we can trim the openAPI clutter out
// and grab just the schema as a map[string]any (which is what k8s wants)
back := cueOpenAPIEncoded{}
err := goyaml.Unmarshal([]byte(str), &back)
if err != nil {
return k8ssys.CustomResourceDefinitionSpecVersion{}, err
}
if len(back.Components.Schemas) != 1 {
// There should only be one schema here...
// TODO: this may change with subresources--but subresources should have defined names
return k8ssys.CustomResourceDefinitionSpecVersion{}, fmt.Errorf("version %s has multiple schemas", name)
}
var def map[string]any
for _, v := range back.Components.Schemas {
ok := false
def, ok = v.(map[string]any)
if !ok {
return k8ssys.CustomResourceDefinitionSpecVersion{},
fmt.Errorf("error generating openapi schema - generated schema has invalid type")
}
}
return k8ssys.CustomResourceDefinitionSpecVersion{
Name: name,
Served: true,
Storage: stored,
Schema: map[string]any{
"openAPIV3Schema": map[string]any{
"properties": map[string]any{
"spec": def,
},
"required": []any{
"spec",
},
"type": "object",
},
},
}, nil
}
func versionString(version thema.SyntacticVersion) string {
return fmt.Sprintf("v%d-%d", version[0], version[1])
}
// Hoisting this out of thema until we resolve the proper approach there
func generateOpenAPI(sch thema.Schema, props kindsys.CoreProperties) (string, error) {
ctx := sch.Underlying().Context()
v := ctx.CompileString(fmt.Sprintf("#%s: _", props.Name))
defpath := cue.MakePath(cue.Def(props.Name))
defsch := v.FillPath(defpath, sch.Underlying())
cfg := &openapi.Config{
NameFunc: func(v cue.Value, path cue.Path) string {
if path.String() == defpath.String() {
return props.Name
}
return ""
},
Info: ast.NewStruct( // doesn't matter, we're throwing it away
"title", ast.NewString(props.Name),
"version", ast.NewString("0.0"),
),
}
f, err := openapi.Generate(defsch, cfg)
if err != nil {
return "", err
}
return cueyaml.Marshal(sch.Lineage().Runtime().Context().BuildFile(f))
}
+1 -1
View File
@@ -8,7 +8,7 @@ import (
"github.com/grafana/thema/encoding/openapi"
)
// GoTypesJenny creates a [OneToOne] that produces Go types for the provided
// GoTypesJenny is a [OneToOne] that produces Go types for the provided
// [thema.Schema].
type GoTypesJenny struct {
ApplyFuncs []dstutil.ApplyFunc
+2 -2
View File
@@ -36,12 +36,12 @@ type (
MainGenerator string
Using []codejen.NamedJenny
From string
Leader string
}
tvars_kind_registry struct {
// Header tvars_autogen_header
PackageName string
KindPackagePrefix string
Kinds []kindsys.Kind
Kinds []kindsys.Core
}
tvars_coremodel_imports struct {
PackageName string
+66
View File
@@ -0,0 +1,66 @@
package {{ .PackageName }}
import (
"encoding/json"
"fmt"
{{range .Kinds }}
{{ .Props.MachineName }} "{{ $.KindPackagePrefix }}/{{ .Props.MachineName }}/crd"{{end}}
"github.com/grafana/grafana/pkg/kindsys"
"github.com/grafana/grafana/pkg/kindsys/k8ssys"
"github.com/grafana/grafana/pkg/registry/corekind"
"gopkg.in/yaml.v3"
)
// Registry is a list of all of Grafana's core structured kinds, wrapped in a
// standard [k8ssys.CRD] interface that makes them usable for interactions
// with certain Kubernetes controller and apimachinery libraries.
//
// There are two access methods: individually via literal named methods, or as
// a slice returned from All() method.
//
// Prefer the individual named methods for use cases where the particular kind(s)
// that are needed are known to the caller. Prefer All() when performing operations
// generically across all kinds.
type Registry struct {
all [{{ len .Kinds }}]k8ssys.Kind
}
{{range $i, $k := .Kinds }}
// {{ .Props.Name }} returns the [k8ssys.Kind] instance for the {{ .Props.Name }} kind.
func (r *Registry) {{ .Props.Name }}() k8ssys.Kind {
return r.all[{{ $i }}]
}
{{end}}
func doNewRegistry(breg *corekind.Base) *Registry {
var err error
var b []byte
var kk k8ssys.Kind
reg := &Registry{}
{{range $i, $k := .Kinds }}
kk = k8ssys.Kind{
GrafanaKind: breg.{{ $k.Props.Name }}(),
Object: &{{ $k.Props.MachineName }}.{{ $k.Props.Name }}{},
ObjectList: &{{ $k.Props.MachineName }}.{{ $k.Props.Name }}List{},
}
// TODO Having the committed form on disk in YAML is worth doing this for now...but fix this silliness
map{{ $i }} := make(map[string]any)
err = yaml.Unmarshal({{ $k.Props.MachineName }}.CRDYaml, map{{ $i }})
if err != nil {
panic(fmt.Sprintf("generated CRD YAML for {{ $k.Props.Name }} failed to unmarshal: %s", err))
}
b, err = json.Marshal(map{{ $i }})
if err != nil {
panic(fmt.Sprintf("could not re-marshal CRD JSON for {{ $k.Props.Name }}: %s", err))
}
err = json.Unmarshal(b, &kk.Schema)
if err != nil {
panic(fmt.Sprintf("could not unmarshal CRD JSON for {{ $k.Props.Name }}: %s", err))
}
reg.all[{{ $i }}] = kk
{{end}}
return reg
}
+25
View File
@@ -0,0 +1,25 @@
package crd
import (
_ "embed"
"github.com/grafana/grafana/pkg/kinds/{{ .Props.MachineName }}"
"github.com/grafana/grafana/pkg/kindsys/k8ssys"
)
// The CRD YAML representation of the {{ .Props.Name }} kind.
//
//go:embed {{ .Props.MachineName }}.crd.yml
var CRDYaml []byte
// {{ .Props.Name }} is the Go CRD representation of a single {{ .Props.Name }} object.
// It implements [runtime.Object], and is used in k8s scheme construction.
type {{ .Props.Name }} struct {
k8ssys.Base[{{ .Props.MachineName }}.{{ .Props.Name }}]
}
// {{ .Props.Name }}List is the Go CRD representation of a list {{ .Props.Name }} objects.
// It implements [runtime.Object], and is used in k8s scheme construction.
type {{ .Props.Name }}List struct {
k8ssys.ListBase[{{ .Props.MachineName }}.{{ .Props.Name }}]
}
+8 -8
View File
@@ -1,11 +1,11 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// {{ .MainGenerator }}
// Using jennies:
{{ .Leader }} Code generated - EDITING IS FUTILE. DO NOT EDIT.
{{ .Leader }}
{{ .Leader }} Generated by:
{{ .Leader }} {{ .MainGenerator }}
{{ .Leader }} Using jennies:
{{- range .Using }}
// {{ .JennyName }}
{{ $.Leader }} {{ .JennyName }}
{{- end }}
//
// Run 'make gen-cue' from repository root to regenerate.
{{ .Leader }}
{{ .Leader }} Run 'make gen-cue' from repository root to regenerate.