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
parent b95eda045a
commit e70d623f90
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1748 additions and 34 deletions

19
go.mod
View File

@ -7,10 +7,6 @@ go 1.19
// Also, use our fork with fixes for unimplemented methods (required for Go 1.16).
replace github.com/denisenkom/go-mssqldb => github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036
// Override k8s.io/client-go outdated dependency, which is an indirect dependency of grafana/loki.
// It's also present on grafana/loki's go.mod so we'll need till it gets updated.
replace k8s.io/client-go => k8s.io/client-go v0.25.0
replace github.com/russellhaering/goxmldsig@v1.1.0 => github.com/russellhaering/goxmldsig v1.1.1
// Avoid using v2.0.0+incompatible Redigo used by dependencies as the latest maintained branch of Redigo is v1.
@ -24,6 +20,17 @@ replace github.com/docker/docker => github.com/moby/moby v0.7.3-0.20190826074503
// contains openapi encoder fixes. remove ASAP
replace cuelang.org/go => github.com/sdboyer/cue v0.5.0-beta.2.0.20221218111347-341999f48bdb
// For some insane reason, client-go seems to have a broken v12.0.0 tag on it that forces us to
// hoist a replace statement.
replace k8s.io/client-go => k8s.io/client-go v0.25.3
require (
k8s.io/api v0.25.3 // indirect
k8s.io/apiextensions-apiserver v0.25.3
k8s.io/apimachinery v0.25.3
k8s.io/client-go v12.0.0+incompatible
)
require (
cloud.google.com/go/storage v1.28.1
cuelang.org/go v0.5.0-beta.2
@ -257,7 +264,6 @@ require (
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.11.2
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.11.2
gocloud.dev v0.25.0
k8s.io/client-go v12.0.0+incompatible // gets replaced with v0.25.0
)
require (
@ -268,7 +274,6 @@ require (
github.com/grafana/thema v0.0.0-20230122235053-b4b6714dd1c9
github.com/hmarr/codeowners v1.1.1
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f
k8s.io/apimachinery v0.25.3
)
require (
@ -296,6 +301,7 @@ require (
github.com/emicklei/go-restful/v3 v3.8.0 // indirect
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1 // indirect
github.com/envoyproxy/protoc-gen-validate v0.6.7 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
@ -333,7 +339,6 @@ require (
golang.org/x/term v0.3.0 // indirect
gopkg.in/fsnotify/fsnotify.v1 v1.4.7 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/api v0.25.3 // indirect
k8s.io/klog/v2 v2.80.0 // indirect
k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect

14
go.sum
View File

@ -770,8 +770,9 @@ github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUork
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/fsouza/fake-gcs-server v1.7.0/go.mod h1:5XIRs4YvwNbNoz+1JF8j6KLAyDh7RHGAyAK3EP2EsNk=
github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA=
github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY=
@ -1839,6 +1840,7 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c=
github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU=
github.com/onsi/ginkgo/v2 v2.1.6 h1:Fx2POJZfKRQcM1pH49qSZiYeu319wji004qX+GDovrU=
github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
@ -1850,6 +1852,7 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo=
github.com/onsi/gomega v1.24.0 h1:+0glovB9Jd6z3VR+ScSwQqXVTIfJcGA9UBM8yzQxhqg=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
@ -2856,6 +2859,7 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
@ -3180,9 +3184,10 @@ k8s.io/api v0.20.4/go.mod h1:++lNL1AJMkDymriNniQsWRkMDzRaX2Y/POTUi8yvqYQ=
k8s.io/api v0.20.6/go.mod h1:X9e8Qag6JV/bL5G6bU8sdVRltWKmdHsFUGS3eVndqE8=
k8s.io/api v0.21.1/go.mod h1:FstGROTmsSHBarKc8bylzXih8BLNYTiS3TZcsoEDg2s=
k8s.io/api v0.22.1/go.mod h1:bh13rkTp3F1XEaLGykbyRD2QaTTzPm0e/BMd8ptFONY=
k8s.io/api v0.25.0/go.mod h1:ttceV1GyV1i1rnmvzT3BST08N6nGt+dudGrquzVQWPk=
k8s.io/api v0.25.3 h1:Q1v5UFfYe87vi5H7NU0p4RXC26PPMT8KOpr1TLQbCMQ=
k8s.io/api v0.25.3/go.mod h1:o42gKscFrEVjHdQnyRenACrMtbuJsVdP+WVjqejfzmI=
k8s.io/apiextensions-apiserver v0.25.3 h1:bfI4KS31w2f9WM1KLGwnwuVlW3RSRPuIsfNF/3HzR0k=
k8s.io/apiextensions-apiserver v0.25.3/go.mod h1:ZJqwpCkxIx9itilmZek7JgfUAM0dnTsA48I4krPqRmo=
k8s.io/apimachinery v0.0.0-20190809020650-423f5d784010/go.mod h1:Waf/xTS2FGRrgXCkO5FP3XxTOWh0qLf2QhL1qFZZ/R8=
k8s.io/apimachinery v0.17.5/go.mod h1:ioIo1G/a+uONV7Tv+ZmCbMG1/a3kVw5YcDdncd8ugQ0=
k8s.io/apimachinery v0.20.1/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
@ -3190,14 +3195,13 @@ k8s.io/apimachinery v0.20.4/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRp
k8s.io/apimachinery v0.20.6/go.mod h1:ejZXtW1Ra6V1O5H8xPBGz+T3+4gfkTCeExAHKU57MAc=
k8s.io/apimachinery v0.21.1/go.mod h1:jbreFvJo3ov9rj7eWT7+sYiRx+qZuCYXwWT1bcDswPY=
k8s.io/apimachinery v0.22.1/go.mod h1:O3oNtNadZdeOMxHFVxOreoznohCpy0z6mocxbZr7oJ0=
k8s.io/apimachinery v0.25.0/go.mod h1:qMx9eAk0sZQGsXGu86fab8tZdffHbwUfsvzqKn4mfB0=
k8s.io/apimachinery v0.25.3 h1:7o9ium4uyUOM76t6aunP0nZuex7gDf8VGwkR5RcJnQc=
k8s.io/apimachinery v0.25.3/go.mod h1:jaF9C/iPNM1FuLl7Zuy5b9v+n35HGSh6AQ4HYRkCqwo=
k8s.io/apiserver v0.20.1/go.mod h1:ro5QHeQkgMS7ZGpvf4tSMx6bBOgPfE+f52KwvXfScaU=
k8s.io/apiserver v0.20.4/go.mod h1:Mc80thBKOyy7tbvFtB4kJv1kbdD0eIH8k8vianJcbFM=
k8s.io/apiserver v0.20.6/go.mod h1:QIJXNt6i6JB+0YQRNcS0hdRHJlMhflFmsBDeSgT1r8Q=
k8s.io/client-go v0.25.0 h1:CVWIaCETLMBNiTUta3d5nzRbXvY5Hy9Dpl+VvREpu5E=
k8s.io/client-go v0.25.0/go.mod h1:lxykvypVfKilxhTklov0wz1FoaUZ8X4EwbhS6rpRfN8=
k8s.io/client-go v0.25.3 h1:oB4Dyl8d6UbfDHD8Bv8evKylzs3BXzzufLiO27xuPs0=
k8s.io/client-go v0.25.3/go.mod h1:t39LPczAIMwycjcXkVc+CB+PZV69jQuNx4um5ORDjQA=
k8s.io/component-base v0.20.1/go.mod h1:guxkoJnNoh8LNrbtiQOlyp2Y2XFCZQmrcg2n/DeYNLk=
k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGwgjI=
k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM=

View File

@ -6,6 +6,8 @@ name: "Dashboard"
maturity: "experimental"
description: "A Grafana dashboard."
crd: dummySchema: true
lineage: seqs: [
{
schemas: [

View File

@ -42,6 +42,9 @@ func main() {
codegen.BaseCoreRegistryJenny(filepath.Join("pkg", "registry", "corekind"), kindsys.GoCoreKindParentPath),
codegen.LatestMajorsOrXJenny(kindsys.TSCoreKindParentPath, codegen.TSTypesJenny{}),
codegen.TSVeneerIndexJenny(filepath.Join("packages", "grafana-schema", "src")),
codegen.CRDTypesJenny(kindsys.GoCoreKindParentPath),
codegen.YamlCRDJenny(kindsys.GoCoreKindParentPath),
codegen.CRDKindRegistryJenny(filepath.Join("pkg", "registry", "corecrd")),
codegen.DocsJenny(filepath.Join("docs", "sources", "developers", "kinds", "core")),
)

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

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

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
}

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
}

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

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

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

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
}

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

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.

View File

@ -0,0 +1,36 @@
# Code generated - EDITING IS FUTILE. DO NOT EDIT.
#
# Generated by:
# kinds/gen.go
# Using jennies:
# YamlCRDJenny
#
# Run 'make gen-cue' from repository root to regenerate.
# This CRD is generated with an empty schema body because Grafana's
# code generators currently produce OpenAPI that Kubernetes will not
# accept, despite being valid.
kind: CustomResourceDefinition
apiVersion: apiextensions.k8s.io/v1
metadata:
name: dashboards.dashboard.core.grafana.com
spec:
group: dashboard.core.grafana.com
versions:
- name: v0-0alpha1
served: true
storage: true
schema:
openAPIV3Schema:
properties:
spec:
type: object
x-kubernetes-preserve-unknown-fields: true
required:
- spec
type: object
names:
kind: Dashboard
plural: dashboards
scope: Namespaced

View File

@ -0,0 +1,34 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// CRDTypesJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package crd
import (
_ "embed"
"github.com/grafana/grafana/pkg/kinds/dashboard"
"github.com/grafana/grafana/pkg/kindsys/k8ssys"
)
// The CRD YAML representation of the Dashboard kind.
//
//go:embed dashboard.crd.yml
var CRDYaml []byte
// Dashboard is the Go CRD representation of a single Dashboard object.
// It implements [runtime.Object], and is used in k8s scheme construction.
type Dashboard struct {
k8ssys.Base[dashboard.Dashboard]
}
// DashboardList is the Go CRD representation of a list Dashboard objects.
// It implements [runtime.Object], and is used in k8s scheme construction.
type DashboardList struct {
k8ssys.ListBase[dashboard.Dashboard]
}

View File

@ -0,0 +1,120 @@
# Code generated - EDITING IS FUTILE. DO NOT EDIT.
#
# Generated by:
# kinds/gen.go
# Using jennies:
# YamlCRDJenny
#
# Run 'make gen-cue' from repository root to regenerate.
kind: CustomResourceDefinition
apiVersion: apiextensions.k8s.io/v1
metadata:
name: librarypanels.librarypanel.core.grafana.com
spec:
group: librarypanel.core.grafana.com
versions:
- name: v0-0alpha1
served: true
storage: true
schema:
openAPIV3Schema:
properties:
spec:
properties:
description:
description: Panel description
type: string
folderUid:
description: Folder UID
type: string
meta:
description: Object storage metadata
properties:
connectedDashboards:
format: int64
type: integer
created:
type: string
createdBy:
properties:
avatarUrl:
type: string
id:
format: int64
type: integer
name:
type: string
required:
- id
- name
- avatarUrl
type: object
folderName:
type: string
folderUid:
type: string
updated:
type: string
updatedBy:
properties:
avatarUrl:
type: string
id:
format: int64
type: integer
name:
type: string
required:
- id
- name
- avatarUrl
type: object
required:
- folderName
- folderUid
- connectedDashboards
- created
- updated
- createdBy
- updatedBy
type: object
model:
description: |-
TODO: should be the same panel schema defined in dashboard
Typescript: Omit<Panel, 'gridPos' | 'id' | 'libraryPanel'>;
type: object
name:
description: Panel name (also saved in the model)
minLength: 1
type: string
schemaVersion:
description: Dashboard version when this was saved (zero if unknown)
maximum: 65535
minimum: 0
type: integer
type:
description: The panel type (from inside the model)
minLength: 1
type: string
uid:
description: Library element UID
type: string
version:
description: panel version, incremented each time the dashboard is updated.
format: int64
type: integer
required:
- uid
- name
- type
- version
- model
type: object
required:
- spec
type: object
names:
kind: LibraryPanel
plural: librarypanels
scope: Namespaced

View File

@ -0,0 +1,34 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// CRDTypesJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package crd
import (
_ "embed"
"github.com/grafana/grafana/pkg/kinds/librarypanel"
"github.com/grafana/grafana/pkg/kindsys/k8ssys"
)
// The CRD YAML representation of the LibraryPanel kind.
//
//go:embed librarypanel.crd.yml
var CRDYaml []byte
// LibraryPanel is the Go CRD representation of a single LibraryPanel object.
// It implements [runtime.Object], and is used in k8s scheme construction.
type LibraryPanel struct {
k8ssys.Base[librarypanel.LibraryPanel]
}
// LibraryPanelList is the Go CRD representation of a list LibraryPanel objects.
// It implements [runtime.Object], and is used in k8s scheme construction.
type LibraryPanelList struct {
k8ssys.ListBase[librarypanel.LibraryPanel]
}

View File

@ -0,0 +1,82 @@
# Code generated - EDITING IS FUTILE. DO NOT EDIT.
#
# Generated by:
# kinds/gen.go
# Using jennies:
# YamlCRDJenny
#
# Run 'make gen-cue' from repository root to regenerate.
kind: CustomResourceDefinition
apiVersion: apiextensions.k8s.io/v1
metadata:
name: playlists.playlist.core.grafana.com
spec:
group: playlist.core.grafana.com
versions:
- name: v0-0alpha1
served: true
storage: true
schema:
openAPIV3Schema:
properties:
spec:
properties:
interval:
default: 5m
description: |-
Interval sets the time between switching views in a playlist.
FIXME: Is this based on a standardized format or what options are available? Can datemath be used?
type: string
items:
description: |-
The ordered list of items that the playlist will iterate over.
FIXME! This should not be optional, but changing it makes the godegen awkward
items:
properties:
title:
description: Title is an unused property -- it will be removed in the future
type: string
type:
description: Type of the item.
enum:
- dashboard_by_uid
- dashboard_by_id
- dashboard_by_tag
type: string
value:
description: |-
Value depends on type and describes the playlist item.
- dashboard_by_id: The value is an internal numerical identifier set by Grafana. This
is not portable as the numerical identifier is non-deterministic between different instances.
Will be replaced by dashboard_by_uid in the future. (deprecated)
- dashboard_by_tag: The value is a tag which is set on any number of dashboards. All
dashboards behind the tag will be added to the playlist.
- dashboard_by_uid: The value is the dashboard UID
type: string
required:
- type
- value
type: object
type: array
name:
description: Name of the playlist.
type: string
uid:
description: |-
Unique playlist identifier. Generated on creation, either by the
creator of the playlist of by the application.
type: string
required:
- uid
- name
- interval
type: object
required:
- spec
type: object
names:
kind: Playlist
plural: playlists
scope: Namespaced

View File

@ -0,0 +1,34 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// CRDTypesJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package crd
import (
_ "embed"
"github.com/grafana/grafana/pkg/kinds/playlist"
"github.com/grafana/grafana/pkg/kindsys/k8ssys"
)
// The CRD YAML representation of the Playlist kind.
//
//go:embed playlist.crd.yml
var CRDYaml []byte
// Playlist is the Go CRD representation of a single Playlist object.
// It implements [runtime.Object], and is used in k8s scheme construction.
type Playlist struct {
k8ssys.Base[playlist.Playlist]
}
// PlaylistList is the Go CRD representation of a list Playlist objects.
// It implements [runtime.Object], and is used in k8s scheme construction.
type PlaylistList struct {
k8ssys.ListBase[playlist.Playlist]
}

View File

@ -0,0 +1,56 @@
# Code generated - EDITING IS FUTILE. DO NOT EDIT.
#
# Generated by:
# kinds/gen.go
# Using jennies:
# YamlCRDJenny
#
# Run 'make gen-cue' from repository root to regenerate.
kind: CustomResourceDefinition
apiVersion: apiextensions.k8s.io/v1
metadata:
name: preferencess.preferences.core.grafana.com
spec:
group: preferences.core.grafana.com
versions:
- name: v0-0alpha1
served: true
storage: true
schema:
openAPIV3Schema:
properties:
spec:
properties:
homeDashboardUID:
description: UID for the home dashboard
type: string
language:
description: Selected language (beta)
type: string
queryHistory:
description: Explore query history preferences
properties:
homeTab:
description: 'one of: '''' | ''query'' | ''starred'';'
type: string
type: object
theme:
description: light, dark, empty is default
type: string
timezone:
description: |-
The timezone selection
TODO: this should use the timezone defined in common
type: string
weekStart:
description: day of the week (sunday, monday, etc)
type: string
type: object
required:
- spec
type: object
names:
kind: Preferences
plural: preferencess
scope: Namespaced

View File

@ -0,0 +1,34 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// CRDTypesJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package crd
import (
_ "embed"
"github.com/grafana/grafana/pkg/kinds/preferences"
"github.com/grafana/grafana/pkg/kindsys/k8ssys"
)
// The CRD YAML representation of the Preferences kind.
//
//go:embed preferences.crd.yml
var CRDYaml []byte
// Preferences is the Go CRD representation of a single Preferences object.
// It implements [runtime.Object], and is used in k8s scheme construction.
type Preferences struct {
k8ssys.Base[preferences.Preferences]
}
// PreferencesList is the Go CRD representation of a list Preferences objects.
// It implements [runtime.Object], and is used in k8s scheme construction.
type PreferencesList struct {
k8ssys.ListBase[preferences.Preferences]
}

View File

@ -0,0 +1,56 @@
# Code generated - EDITING IS FUTILE. DO NOT EDIT.
#
# Generated by:
# kinds/gen.go
# Using jennies:
# YamlCRDJenny
#
# Run 'make gen-cue' from repository root to regenerate.
kind: CustomResourceDefinition
apiVersion: apiextensions.k8s.io/v1
metadata:
name: publicdashboards.publicdashboard.core.grafana.com
spec:
group: publicdashboard.core.grafana.com
versions:
- name: v0-0alpha1
served: true
storage: true
schema:
openAPIV3Schema:
properties:
spec:
properties:
accessToken:
description: Unique public access token
type: string
annotationsEnabled:
description: Flag that indicates if annotations are enabled
type: boolean
dashboardUid:
description: Dashboard unique identifier referenced by this public dashboard
type: string
isEnabled:
description: Flag that indicates if the public dashboard is enabled
type: boolean
timeSelectionEnabled:
description: Flag that indicates if the time range picker is enabled
type: boolean
uid:
description: Unique public dashboard identifier
type: string
required:
- uid
- dashboardUid
- isEnabled
- annotationsEnabled
- timeSelectionEnabled
type: object
required:
- spec
type: object
names:
kind: PublicDashboard
plural: publicdashboards
scope: Namespaced

View File

@ -0,0 +1,34 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// CRDTypesJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package crd
import (
_ "embed"
"github.com/grafana/grafana/pkg/kinds/publicdashboard"
"github.com/grafana/grafana/pkg/kindsys/k8ssys"
)
// The CRD YAML representation of the PublicDashboard kind.
//
//go:embed publicdashboard.crd.yml
var CRDYaml []byte
// PublicDashboard is the Go CRD representation of a single PublicDashboard object.
// It implements [runtime.Object], and is used in k8s scheme construction.
type PublicDashboard struct {
k8ssys.Base[publicdashboard.PublicDashboard]
}
// PublicDashboardList is the Go CRD representation of a list PublicDashboard objects.
// It implements [runtime.Object], and is used in k8s scheme construction.
type PublicDashboardList struct {
k8ssys.ListBase[publicdashboard.PublicDashboard]
}

View File

@ -0,0 +1,94 @@
# Code generated - EDITING IS FUTILE. DO NOT EDIT.
#
# Generated by:
# kinds/gen.go
# Using jennies:
# YamlCRDJenny
#
# Run 'make gen-cue' from repository root to regenerate.
kind: CustomResourceDefinition
apiVersion: apiextensions.k8s.io/v1
metadata:
name: serviceaccounts.serviceaccount.core.grafana.com
spec:
group: serviceaccount.core.grafana.com
versions:
- name: v0-0alpha1
served: true
storage: true
schema:
openAPIV3Schema:
properties:
spec:
properties:
accessControl:
additionalProperties:
type: boolean
description: AccessControl metadata associated with a given resource.
type: object
avatarUrl:
description: |-
AvatarUrl is the service account's avatar URL. It allows the frontend to display a picture in front
of the service account.
type: string
created:
description: Created indicates when the service account was created.
format: int64
type: integer
id:
description: ID is the unique identifier of the service account in the database.
format: int64
type: integer
isDisabled:
description: IsDisabled indicates if the service account is disabled.
type: boolean
login:
description: Login of the service account.
type: string
name:
description: Name of the service account.
type: string
orgId:
description: OrgId is the ID of an organisation the service account belongs to.
format: int64
type: integer
role:
description: Role is the Grafana organization role of the service account which can be 'Viewer', 'Editor', 'Admin'.
enum:
- Admin
- Editor
- Viewer
type: string
teams:
description: Teams is a list of teams the service account belongs to.
items:
type: string
type: array
tokens:
description: |-
Tokens is the number of active tokens for the service account.
Tokens are used to authenticate the service account against Grafana.
format: int64
type: integer
updated:
description: Updated indicates when the service account was updated.
format: int64
type: integer
required:
- id
- orgId
- name
- login
- isDisabled
- role
- tokens
- avatarUrl
type: object
required:
- spec
type: object
names:
kind: ServiceAccount
plural: serviceaccounts
scope: Namespaced

View File

@ -0,0 +1,34 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// CRDTypesJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package crd
import (
_ "embed"
"github.com/grafana/grafana/pkg/kinds/serviceaccount"
"github.com/grafana/grafana/pkg/kindsys/k8ssys"
)
// The CRD YAML representation of the ServiceAccount kind.
//
//go:embed serviceaccount.crd.yml
var CRDYaml []byte
// ServiceAccount is the Go CRD representation of a single ServiceAccount object.
// It implements [runtime.Object], and is used in k8s scheme construction.
type ServiceAccount struct {
k8ssys.Base[serviceaccount.ServiceAccount]
}
// ServiceAccountList is the Go CRD representation of a list ServiceAccount objects.
// It implements [runtime.Object], and is used in k8s scheme construction.
type ServiceAccountList struct {
k8ssys.ListBase[serviceaccount.ServiceAccount]
}

View File

@ -0,0 +1,77 @@
# Code generated - EDITING IS FUTILE. DO NOT EDIT.
#
# Generated by:
# kinds/gen.go
# Using jennies:
# YamlCRDJenny
#
# Run 'make gen-cue' from repository root to regenerate.
kind: CustomResourceDefinition
apiVersion: apiextensions.k8s.io/v1
metadata:
name: teams.team.core.grafana.com
spec:
group: team.core.grafana.com
versions:
- name: v0-0alpha1
served: true
storage: true
schema:
openAPIV3Schema:
properties:
spec:
properties:
accessControl:
additionalProperties:
type: boolean
description: AccessControl metadata associated with a given resource.
type: object
avatarUrl:
description: AvatarUrl is the team's avatar URL.
type: string
created:
description: Created indicates when the team was created.
format: int64
type: integer
email:
description: Email of the team.
type: string
memberCount:
description: MemberCount is the number of the team members.
format: int64
type: integer
name:
description: Name of the team.
type: string
orgId:
description: OrgId is the ID of an organisation the team belongs to.
format: int64
type: integer
permission:
description: TODO - it seems it's a team_member.permission, unlikely it should belong to the team kind
enum:
- 0
- 1
- 2
- 4
type: integer
updated:
description: Updated indicates when the team was updated.
format: int64
type: integer
required:
- orgId
- name
- memberCount
- permission
- created
- updated
type: object
required:
- spec
type: object
names:
kind: Team
plural: teams
scope: Namespaced

View File

@ -0,0 +1,34 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// CRDTypesJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package crd
import (
_ "embed"
"github.com/grafana/grafana/pkg/kinds/team"
"github.com/grafana/grafana/pkg/kindsys/k8ssys"
)
// The CRD YAML representation of the Team kind.
//
//go:embed team.crd.yml
var CRDYaml []byte
// Team is the Go CRD representation of a single Team object.
// It implements [runtime.Object], and is used in k8s scheme construction.
type Team struct {
k8ssys.Base[team.Team]
}
// TeamList is the Go CRD representation of a list Team objects.
// It implements [runtime.Object], and is used in k8s scheme construction.
type TeamList struct {
k8ssys.ListBase[team.Team]
}

154
pkg/kindsys/k8ssys/crd.go Normal file
View File

@ -0,0 +1,154 @@
package k8ssys
import (
"fmt"
"reflect"
"github.com/grafana/grafana/pkg/kindsys"
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
k8schema "k8s.io/apimachinery/pkg/runtime/schema"
)
type Kind struct {
GrafanaKind kindsys.Kind
Object runtime.Object // singular type
ObjectList runtime.Object // list type
Schema apiextensionsv1.CustomResourceDefinition
}
// TODO this could probably be done in CUE/framework
func (crd Kind) GVK() k8schema.GroupVersionKind {
// TODO custom structured
props := crd.GrafanaKind.Props().(kindsys.CoreProperties)
gvk := k8schema.GroupVersionKind{
Group: fmt.Sprintf("%s.core.grafana.com", props.MachineName),
Kind: props.Name,
}
if props.Maturity.Less(kindsys.MaturityStable) {
gvk.Version = "v0-0alpha1"
} else {
gvk.Version = fmt.Sprintf("v%d-%d", props.CurrentVersion[0], props.CurrentVersion[1])
}
return gvk
}
// CustomResourceDefinitionList is the kubernetes-API-compliant representation of a list of CustomResourceDefinitions
type CustomResourceDefinitionList struct {
ListBase[CustomResourceDefinition]
}
// CustomResourceDefinition is the kubernetes-API-compliant representation of a Custom Resource Definition
type CustomResourceDefinition struct {
metav1.TypeMeta `json:",inline" yaml:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty"`
Spec CustomResourceDefinitionSpec `json:"spec"`
}
// DeepCopyObject implements runtime.Object.
func (crd *CustomResourceDefinition) DeepCopyObject() runtime.Object {
return DeepCopyObject(crd)
}
// CustomResourceDefinitionSpec is the body or spec of a kubernetes Custom Resource Definition
type CustomResourceDefinitionSpec struct {
Group string `json:"group" yaml:"group"`
Versions []CustomResourceDefinitionSpecVersion `json:"versions" yaml:"versions"`
Names CustomResourceDefinitionSpecNames `json:"names" yaml:"names"`
Scope string `json:"scope" yaml:"scope"`
}
// CustomResourceDefinitionSpecVersion is the representation of a specific version of a CRD, as part of the overall spec
type CustomResourceDefinitionSpecVersion struct {
Name string `json:"name" yaml:"name"`
Served bool `json:"served" yaml:"served"`
Storage bool `json:"storage" yaml:"storage"`
Schema map[string]any `json:"schema" yaml:"schema"`
Subresources map[string]any `json:"subresources,omitempty" yaml:"subresources,omitempty"`
}
// CustomResourceDefinitionSpecNames is the struct representing the names (kind and plural) of a kubernetes CRD
type CustomResourceDefinitionSpecNames struct {
Kind string `json:"kind" yaml:"kind"`
Plural string `json:"plural" yaml:"plural"`
}
// Base is a struct which describes a basic CRD, and implements runtime.Object.
// SpecType should be the struct that represents the spec in the definition.
// It cannot be used on its own, as the name of the CRD in kubernetes must exactly match the name of struct.
// Instead, this struct can be used as a component of a new named struct, for examples:
//
// type MyCustomResource struct {
// crd.Base[MyCustomResourceSpec]
// }
type Base[SpecType any] struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec SpecType `json:"spec"`
}
// DeepCopyObject is implemented for Base so it will implement runtime.Object.
// DeepCopyObject here just calls crd.DeepCopyObject on itself.
func (b *Base[T]) DeepCopyObject() runtime.Object {
return DeepCopyObject(b)
}
// ListBase is a struct which describes a list of CRDs, and implements runtime.Object.
// ItemType should be the CRD type being listed (NOT the model).
// It cannot be used on its own, as the struct name must exactly match `<name of kubernetes CRD>List`.
// Instead, this struct can be used as a component of a new named struct, for examples:
//
// type MyCustomResourceList struct {
// crd.Base[MyCustomResource]
// }
type ListBase[ItemType any] struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`
Items []ItemType `json:"items"`
}
// DeepCopyObject is implemented for Base so it will implement runtime.Object.
// DeepCopyObject here just calls crd.DeepCopyObject on itself.
func (b *ListBase[T]) DeepCopyObject() runtime.Object {
return DeepCopyObject(b)
}
// BaseStatus extends Base by including a Status subresource.
// This should be used if your kubernetes CRD includes the status subresource and you want to be able to view/modify it.
// Usage is identical to Base
type BaseStatus[SpecType, StatusType any] struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec SpecType `json:"spec"`
Status StatusType `json:"status"`
}
// DeepCopyObject is implemented for Base so it will implement runtime.Object.
// DeepCopyObject here just calls crd.DeepCopyObject on itself.
func (b *BaseStatus[T, S]) DeepCopyObject() runtime.Object {
return DeepCopyObject(b)
}
// DeepCopyObject is an implementation of the receiver method required for implementing runtime.Object.
// It should be used in your own runtime.Object implementations if you do not wish to implement custom behavior.
// Example:
//
// func (c *CustomObject) DeepCopyObject() runtime.Object {
// return crd.DeepCopyObject(c)
// }
func DeepCopyObject(in any) runtime.Object {
val := reflect.ValueOf(in).Elem()
cpy := reflect.New(val.Type())
cpy.Elem().Set(val)
// Using the <obj>, <ok> for the type conversion ensures that it doesn't panic if it can't be converted
if obj, ok := cpy.Interface().(runtime.Object); ok {
return obj
}
// TODO: better return than nil?
return nil
}

View File

@ -100,6 +100,28 @@ Core: S=close({
lineage: { name: S.machineName }
lineageIsGroup: false
// crd contains properties specific to converting this kind to a Kubernetes CRD.
crd: {
// group is used as the CRD group name in the GVK.
group: "\(S.machineName).core.grafana.com"
// scope determines whether resources of this kind exist globally ("Cluster") or
// within Kubernetes namespaces.
scope: "Cluster" | *"Namespaced"
// dummySchema determines whether a dummy OpenAPI schema - where the schema is
// simply an empty, open object - should be generated for the kind.
//
// It is a goal that this option eventually be force dto false. Only set to
// true when Grafana's code generators produce OpenAPI that is rejected by
// Kubernetes' CRD validation.
dummySchema: bool | *false
// deepCopy determines whether a generic implementation of copying should be
// generated, or a passthrough call to a Go function.
// deepCopy: *"generic" | "passthrough"
}
})
nonEmptyString: string & strings.MinRunes(1)

View File

@ -23,6 +23,11 @@ type CommonProperties struct {
type CoreProperties struct {
CommonProperties
CurrentVersion thema.SyntacticVersion `json:"currentVersion"`
CRD struct {
Group string `json:"group"`
Scope string `json:"scope"`
DummySchema bool `json:"dummySchema"`
} `json:"crd"`
}
func (m CoreProperties) _private() {}

View File

@ -117,6 +117,11 @@
"apikey": {
"category": "core",
"codeowners": [],
"crd": {
"dummySchema": false,
"group": "",
"scope": ""
},
"currentVersion": [
0,
0
@ -279,6 +284,11 @@
"grafana/plugins-platform-frontend",
"grafana/user-essentials"
],
"crd": {
"dummySchema": true,
"group": "dashboard.core.grafana.com",
"scope": "Namespaced"
},
"currentVersion": [
0,
0
@ -369,6 +379,11 @@
"datasource": {
"category": "core",
"codeowners": [],
"crd": {
"dummySchema": false,
"group": "",
"scope": ""
},
"currentVersion": [
0,
0
@ -478,6 +493,11 @@
"folder": {
"category": "core",
"codeowners": [],
"crd": {
"dummySchema": false,
"group": "",
"scope": ""
},
"currentVersion": [
0,
0
@ -816,6 +836,11 @@
"grafana/plugins-platform-frontend",
"grafana/user-essentials"
],
"crd": {
"dummySchema": false,
"group": "librarypanel.core.grafana.com",
"scope": "Namespaced"
},
"currentVersion": [
0,
0
@ -1181,6 +1206,11 @@
"grafana/plugins-platform-frontend",
"grafana/user-essentials"
],
"crd": {
"dummySchema": false,
"group": "playlist.core.grafana.com",
"scope": "Namespaced"
},
"currentVersion": [
0,
0
@ -1252,6 +1282,11 @@
"grafana/plugins-platform-frontend",
"grafana/user-essentials"
],
"crd": {
"dummySchema": false,
"group": "preferences.core.grafana.com",
"scope": "Namespaced"
},
"currentVersion": [
0,
0
@ -1323,6 +1358,11 @@
"grafana/plugins-platform-frontend",
"grafana/user-essentials"
],
"crd": {
"dummySchema": false,
"group": "publicdashboard.core.grafana.com",
"scope": "Namespaced"
},
"currentVersion": [
0,
0
@ -1345,6 +1385,11 @@
"query": {
"category": "core",
"codeowners": [],
"crd": {
"dummySchema": false,
"group": "",
"scope": ""
},
"currentVersion": [
0,
0
@ -1366,6 +1411,11 @@
"queryhistory": {
"category": "core",
"codeowners": [],
"crd": {
"dummySchema": false,
"group": "",
"scope": ""
},
"currentVersion": [
0,
0
@ -1392,6 +1442,11 @@
"grafana/plugins-platform-frontend",
"grafana/user-essentials"
],
"crd": {
"dummySchema": false,
"group": "serviceaccount.core.grafana.com",
"scope": "Namespaced"
},
"currentVersion": [
0,
0
@ -1513,6 +1568,11 @@
"grafana/plugins-platform-frontend",
"grafana/user-essentials"
],
"crd": {
"dummySchema": false,
"group": "team.core.grafana.com",
"scope": "Namespaced"
},
"currentVersion": [
0,
0
@ -1651,6 +1711,11 @@
"thumb": {
"category": "core",
"codeowners": [],
"crd": {
"dummySchema": false,
"group": "",
"scope": ""
},
"currentVersion": [
0,
0
@ -1694,6 +1759,11 @@
"user": {
"category": "core",
"codeowners": [],
"crd": {
"dummySchema": false,
"group": "",
"scope": ""
},
"currentVersion": [
0,
0

View File

@ -0,0 +1,26 @@
package corecrd
import (
"github.com/grafana/grafana/pkg/kindsys/k8ssys"
"github.com/grafana/grafana/pkg/registry/corekind"
"github.com/grafana/thema"
)
// New constructs a new [Registry].
//
// All calling code within grafana/grafana is expected to use Grafana's
// singleton [thema.Runtime], returned from [cuectx.GrafanaThemaRuntime]. If nil
// is passed, the singleton will be used.
func New(rt *thema.Runtime) *Registry {
breg := corekind.NewBase(rt)
return doNewRegistry(breg)
}
// All returns a slice of all core Grafana CRDs in the registry.
//
// The returned slice is guaranteed to be alphabetically sorted by kind name.
func (r *Registry) All() []k8ssys.Kind {
all := make([]k8ssys.Kind, len(r.all))
copy(all, r.all[:])
return all
}

View File

@ -0,0 +1,231 @@
// Code generated - EDITING IS FUTILE. DO NOT EDIT.
//
// Generated by:
// kinds/gen.go
// Using jennies:
// CRDKindRegistryJenny
//
// Run 'make gen-cue' from repository root to regenerate.
package corecrd
import (
"encoding/json"
"fmt"
dashboard "github.com/grafana/grafana/pkg/kinds/dashboard/crd"
librarypanel "github.com/grafana/grafana/pkg/kinds/librarypanel/crd"
playlist "github.com/grafana/grafana/pkg/kinds/playlist/crd"
preferences "github.com/grafana/grafana/pkg/kinds/preferences/crd"
publicdashboard "github.com/grafana/grafana/pkg/kinds/publicdashboard/crd"
serviceaccount "github.com/grafana/grafana/pkg/kinds/serviceaccount/crd"
team "github.com/grafana/grafana/pkg/kinds/team/crd"
"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 [7]k8ssys.Kind
}
// Dashboard returns the [k8ssys.Kind] instance for the Dashboard kind.
func (r *Registry) Dashboard() k8ssys.Kind {
return r.all[0]
}
// LibraryPanel returns the [k8ssys.Kind] instance for the LibraryPanel kind.
func (r *Registry) LibraryPanel() k8ssys.Kind {
return r.all[1]
}
// Playlist returns the [k8ssys.Kind] instance for the Playlist kind.
func (r *Registry) Playlist() k8ssys.Kind {
return r.all[2]
}
// Preferences returns the [k8ssys.Kind] instance for the Preferences kind.
func (r *Registry) Preferences() k8ssys.Kind {
return r.all[3]
}
// PublicDashboard returns the [k8ssys.Kind] instance for the PublicDashboard kind.
func (r *Registry) PublicDashboard() k8ssys.Kind {
return r.all[4]
}
// ServiceAccount returns the [k8ssys.Kind] instance for the ServiceAccount kind.
func (r *Registry) ServiceAccount() k8ssys.Kind {
return r.all[5]
}
// Team returns the [k8ssys.Kind] instance for the Team kind.
func (r *Registry) Team() k8ssys.Kind {
return r.all[6]
}
func doNewRegistry(breg *corekind.Base) *Registry {
var err error
var b []byte
var kk k8ssys.Kind
reg := &Registry{}
kk = k8ssys.Kind{
GrafanaKind: breg.Dashboard(),
Object: &dashboard.Dashboard{},
ObjectList: &dashboard.DashboardList{},
}
// TODO Having the committed form on disk in YAML is worth doing this for now...but fix this silliness
map0 := make(map[string]any)
err = yaml.Unmarshal(dashboard.CRDYaml, map0)
if err != nil {
panic(fmt.Sprintf("generated CRD YAML for Dashboard failed to unmarshal: %s", err))
}
b, err = json.Marshal(map0)
if err != nil {
panic(fmt.Sprintf("could not re-marshal CRD JSON for Dashboard: %s", err))
}
err = json.Unmarshal(b, &kk.Schema)
if err != nil {
panic(fmt.Sprintf("could not unmarshal CRD JSON for Dashboard: %s", err))
}
reg.all[0] = kk
kk = k8ssys.Kind{
GrafanaKind: breg.LibraryPanel(),
Object: &librarypanel.LibraryPanel{},
ObjectList: &librarypanel.LibraryPanelList{},
}
// TODO Having the committed form on disk in YAML is worth doing this for now...but fix this silliness
map1 := make(map[string]any)
err = yaml.Unmarshal(librarypanel.CRDYaml, map1)
if err != nil {
panic(fmt.Sprintf("generated CRD YAML for LibraryPanel failed to unmarshal: %s", err))
}
b, err = json.Marshal(map1)
if err != nil {
panic(fmt.Sprintf("could not re-marshal CRD JSON for LibraryPanel: %s", err))
}
err = json.Unmarshal(b, &kk.Schema)
if err != nil {
panic(fmt.Sprintf("could not unmarshal CRD JSON for LibraryPanel: %s", err))
}
reg.all[1] = kk
kk = k8ssys.Kind{
GrafanaKind: breg.Playlist(),
Object: &playlist.Playlist{},
ObjectList: &playlist.PlaylistList{},
}
// TODO Having the committed form on disk in YAML is worth doing this for now...but fix this silliness
map2 := make(map[string]any)
err = yaml.Unmarshal(playlist.CRDYaml, map2)
if err != nil {
panic(fmt.Sprintf("generated CRD YAML for Playlist failed to unmarshal: %s", err))
}
b, err = json.Marshal(map2)
if err != nil {
panic(fmt.Sprintf("could not re-marshal CRD JSON for Playlist: %s", err))
}
err = json.Unmarshal(b, &kk.Schema)
if err != nil {
panic(fmt.Sprintf("could not unmarshal CRD JSON for Playlist: %s", err))
}
reg.all[2] = kk
kk = k8ssys.Kind{
GrafanaKind: breg.Preferences(),
Object: &preferences.Preferences{},
ObjectList: &preferences.PreferencesList{},
}
// TODO Having the committed form on disk in YAML is worth doing this for now...but fix this silliness
map3 := make(map[string]any)
err = yaml.Unmarshal(preferences.CRDYaml, map3)
if err != nil {
panic(fmt.Sprintf("generated CRD YAML for Preferences failed to unmarshal: %s", err))
}
b, err = json.Marshal(map3)
if err != nil {
panic(fmt.Sprintf("could not re-marshal CRD JSON for Preferences: %s", err))
}
err = json.Unmarshal(b, &kk.Schema)
if err != nil {
panic(fmt.Sprintf("could not unmarshal CRD JSON for Preferences: %s", err))
}
reg.all[3] = kk
kk = k8ssys.Kind{
GrafanaKind: breg.PublicDashboard(),
Object: &publicdashboard.PublicDashboard{},
ObjectList: &publicdashboard.PublicDashboardList{},
}
// TODO Having the committed form on disk in YAML is worth doing this for now...but fix this silliness
map4 := make(map[string]any)
err = yaml.Unmarshal(publicdashboard.CRDYaml, map4)
if err != nil {
panic(fmt.Sprintf("generated CRD YAML for PublicDashboard failed to unmarshal: %s", err))
}
b, err = json.Marshal(map4)
if err != nil {
panic(fmt.Sprintf("could not re-marshal CRD JSON for PublicDashboard: %s", err))
}
err = json.Unmarshal(b, &kk.Schema)
if err != nil {
panic(fmt.Sprintf("could not unmarshal CRD JSON for PublicDashboard: %s", err))
}
reg.all[4] = kk
kk = k8ssys.Kind{
GrafanaKind: breg.ServiceAccount(),
Object: &serviceaccount.ServiceAccount{},
ObjectList: &serviceaccount.ServiceAccountList{},
}
// TODO Having the committed form on disk in YAML is worth doing this for now...but fix this silliness
map5 := make(map[string]any)
err = yaml.Unmarshal(serviceaccount.CRDYaml, map5)
if err != nil {
panic(fmt.Sprintf("generated CRD YAML for ServiceAccount failed to unmarshal: %s", err))
}
b, err = json.Marshal(map5)
if err != nil {
panic(fmt.Sprintf("could not re-marshal CRD JSON for ServiceAccount: %s", err))
}
err = json.Unmarshal(b, &kk.Schema)
if err != nil {
panic(fmt.Sprintf("could not unmarshal CRD JSON for ServiceAccount: %s", err))
}
reg.all[5] = kk
kk = k8ssys.Kind{
GrafanaKind: breg.Team(),
Object: &team.Team{},
ObjectList: &team.TeamList{},
}
// TODO Having the committed form on disk in YAML is worth doing this for now...but fix this silliness
map6 := make(map[string]any)
err = yaml.Unmarshal(team.CRDYaml, map6)
if err != nil {
panic(fmt.Sprintf("generated CRD YAML for Team failed to unmarshal: %s", err))
}
b, err = json.Marshal(map6)
if err != nil {
panic(fmt.Sprintf("could not re-marshal CRD JSON for Team: %s", err))
}
err = json.Unmarshal(b, &kk.Schema)
if err != nil {
panic(fmt.Sprintf("could not unmarshal CRD JSON for Team: %s", err))
}
reg.all[6] = kk
return reg
}