mirror of
https://github.com/grafana/grafana.git
synced 2025-02-10 23:55:47 -06:00
* extract kindsys * reinstate kindsys report This may end up living somewhere else (or not! who knows!), but the important part is that I don't get rid of it right now :) I hate the package layout (kindsysreport/codegen) for the main function and will take pretty much any alternative suggestion, but we can change also change it later. Note that the generated report.json is in a different location - anything using this (ops something) needs to be updated. * kindsysreport in codeowners
216 lines
6.3 KiB
Go
216 lines
6.3 KiB
Go
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/kindsys/k8ssys"
|
|
"github.com/grafana/thema"
|
|
goyaml "gopkg.in/yaml.v3"
|
|
|
|
"github.com/grafana/kindsys"
|
|
)
|
|
|
|
// 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))
|
|
}
|