The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/codegen/jenny_crd_yaml.go

215 lines
6.3 KiB

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