The preferred way of writing kinds for use with codegen provided by the grafana-app-sdk
CLI is using CUE (support for other input types in the future). If you are familiar with CUE, the base definition of a kind exists in codegen/cuekind/def.cue. However, given that a CUE definition may not be the easiest to understand, especially if you lack familiarity with CUE, we will go in-depth into writing kinds in CUE here. No prior CUE knowledge or experience is required.
Tip
You can generate a kind with descriptive comments of all fields with grafana-app-sdk project kind add <KindName>
Defining a kind can be thought of as being split into two parts: the kind metadata, and the schemas for each version. A simple kind, without any versions (which makes it invalid, but it's a place to start) would look like this:
foo: {
// kind is the kind name. It must be capitalized by convention
kind: "Foo"
// Collection of all versions for the kind, as a map of version string => version details
versions: {}
}
Note
For a kind to be expressed by your app and work with the codegen, it must be a part of your app's Manifest.
When using grafana-app-sdk project kind add
, the newly-created kind is automatically added to your manifest.
To complete the kind, it needs a version and a schema for that version. These slot into the kind like so:
foo: {
// ... existing fields ...
// We have to specify the currentVersion, even if there's only one version
currentVersion: "v1"
// Collection of all versions for the kind, as a map of version string => version details
versions: {
// This is our "v1" version
"v1": {
// schema is the only required field for a version, and contains the schema for this version of the kind
schema: {
// ... schema goes here ...
}
}
}
}
What is the schema
? It's the template for the data. If you're familiar with OpenAPI, the schema is rendered into a subset of OpenAPI when converted into a Custom Resource Definition for the kubernetes API server. In CUE, a schema follows the pattern of a definition, which declares field names and types. Something like:
{
field1: string
field2: int64
field3: bool
field4: float64
}
Is the format of a definition. The declarative style is similar to TypeScript, and it uses go types. You can add additional restrictions as well:
{
positiveNumber: int64 & >0
}
The schema
of an API resource also has a few restrictions on it: there MUST be a spec
field (and this field SHOULD be a struct type), and any other top-level field in the schema
will be considered to be a subresource within the kubernetes API. At present, only status
and scale
are supported for Custom Resource Definitions (CRDs), so other subresource fields will not be supported in your CRD.
With all that, let's complete our simple kind:
foo: {
kind: "Foo"
currentVersion: "v1"
versions: {
"v1": {
schema: {
spec: {
stringField: string
intField: int64
}
}
}
}
}
For this to be a valid CUE file, it needs a package
which should be the directory in which it lives. You'll also need to have initialized a CUE module. grafana-app-sdk project init
does this for your automatically (creating the kinds
directory), but if you want to do it yourself, you'll need to install CUE and run cue mod init
.
Our final CUE file looks like:
package kinds
foo: {
kind: "Foo"
currentVersion: "v1"
versions: {
"v1": {
schema: {
spec: {
stringField: string
intField: int64
}
}
}
}
}
We now have a valid kind! If you save this as a CUE file (.cue
) in your project (the default directory for parsing kinds is ./kinds
), you can now generate code and a CRD file for your kind.
To do so, make sure you have the grafana-app-sdk
CLI installed (you can download a binary for your distribution on the releases page, build the binary from the repo with make build
, or use go install
with the cloned repo (there is a known issue with replace
in the go.mod
that prevents go install
working from a remote source)).
Make sure your kind is added to your manifest. If you set up your project with grafana-app-sdk project init
, you'll already have a kind/manifest.cue
file, but if you don't, a simple manifest looks like this:
package kinds // Or the package you're using for your CUE
manifest: {
appName: "my-app"
kinds: [foo] // This points to the kind `foo` we defined in our file
}
Now you can run
grafana-app-sdk generate
(if you saved your CUE file to a directory different than ./kinds
, add -c <CUE directory>
)
Generated code by default ends up in three different places (these directories can be customized with CLI flags, use grafana-app-sdk generate --help
to display them):
pkg/generated/resource/foo/v1
plugin/src/generated/foo/v1
definitions/
All generated go code ends up in pkg/generated/resource/<kind name>/<kind version>
. For each kind, there are at least six files that are generated (at least six, because each subresource generates its own go file):
foo_codec_gen.go
contains information for the kind to use to encode/decode the go typefoo_metadata_gen.go
is a file that exists for legacy support, and will be eventually removed from codegenfoo_object_gen.go
is a file that contains theFoo
type, which implementsresource.Object
. For more information onresource.Object
, see Using Kinds or Resource Objectsfoo_schema_gen.go
is a file that contains functions for returning aresource.Kind
andresource.Schema
(Kind()
andSchema()
respectively). For more details onresource.Kind
, see Using Kindsfoo_spec_gen.go
is a file that contains a type declaration for theSpec
type, as defined in our CUE. It is used byFoo
infoo_object_gen.go
foo_status_gen.go
is a file that contains a type declaration for theStatus
type, as defined in our CUE. We didn't define astatus
subresource, but there is always a "basic" status subresource for each app platform object that contains some generic data. You can see its definition either in the go code, or as part of the CUE definition of a schema.
Additional foo_x_gen.go
files will be generated for each subresource in your schema (and will be added as a field in Foo
).
To use this generated code in your project, see Using Kinds, Operators & Event-Based Design, Resource Objects, or Resource Stores.
All generated TypeScript code ends up in plugin/src/generated/<kind name>/<kind version>
. For each kind, there are at least three files that are generated (at least three, because each subresource generates its own TypeScript file):
foo_object_gen.ts
contains theFoo
interface, which is compatible with the kubernetes API server definition of theFoo
kind for that version.types.spec.gen.ts
contains theSpec
interface, defined by our CUEspec
fieldtypes.status.gen.ts
contains theStatus
interface, defined by our CUEstatus
field
Additional types.x.gen.ts
files will be generated for each subresource in your schema (and will be added as a field in Foo
).
The definitions
directory holds a JSON (or YAML, depending on CLI flags) Custom Resource Definition (CRD) file for each of your kinds. These files can be applied to a kubernetes API server to generate CRDs for your kinds, which you can then use the other generated code to interface with. For more about CRDs see Kubernetes Concepts.
This directory also holds a generated JSON (or YAML) manifest for your app. This is a file which will be used in the future to register your app with the grafana API server, without needing to work with CRD's and RBAC.
You can turn on or off code generation for front-end (TypeScript) and/or back-end (go) using the codegen
property in your kind or version(s) in your CUE kind. The codegen
field by default looks like:
codegen: {
frontend: true
backend: true
}
And can be overwritten at either the kind level, or the version level (version level will take precedence over the kind level declaration). For example, if we wanted to turn off front-end code from being generated for our kind, but keep it on for version v2
, we could write a kind like this:
myKind: {
kind: "MyKind"
current: "v2"
codegen: {
frontend: false // Turn off front-end codegen for this kind
}
versions: {
"v1": {
schema: {
spec: {
foo: string
}
}
}
"v2": {
schema: {
spec: {
foo: string
bar: int64
}
}
codegen: frontend: true // Turn on front-end codegen for this version
}
}
}
(Here we also introduce a convience of CUE: nested struct fields in one line using the :
separator. We also have a second entry in versions
in our kind, for more details on multiple versions in a kind see Managing Multiple Kind Versions)
To mark a field as optional, like in TypeScript, we use a ?
before the :
. This results in it not being listed as required
in the OpenAPI specification used for the CRD, and the field type in go uses a pointer. For example:
{
foo?: string
}
generates
type Spec struct {
Foo *string `json:"foo,omitempty"`
}
and
export interface Spec {
foo?: string;
}
Often your schemas won't be as simple as the example we wrote, and will need sub-types. You can declare these as inline structs in CUE like
{
foo: string
bar: {
foobar: string
}
}
But you'll end up with go code that isn't very easy to use:
type Spec struct {
Foo string `json:"foo"`
Bar struct{
Foobar string `json:"foobar"`
} `json:"bar"`
}
To generate go types which are more usable, you'll want to embed CUE definitions. This is simpler than it sounds: all you need to do is define a field that begins with a #
. This is a definition, and won't be rendered as a field in the generated go, but you can use it as a type, and it will be turned into a go struct with that type name. Here's our example above adjusted to use a CUE definition:
{
#Bar: {
foobar: string
}
foo: string
bar: #Bar
}
Now we get more usable go code:
type Spec struct {
Bar SpecBar `json:"bar"`
Foo string `json:"foo"`
}
// SpecBar defines model for spec.#Bar.
type SpecBar struct {
Foobar string `json:"foobar"`
}
A definition can be defined anywhere in the schema, so you could define several definitions outside of spec
and still use them within spec
or any other subresource.
You can import go types, such as time.Time
using import
at the top of your CUE file. However, for codegen to properly handle time.Time
, you need to union it with string
, like so:
package kinds
import "time"
foo: {
kind: "Foo"
currentVersion: "v1"
versions: {
"v1": {
schema: {
spec: {
timeField: string & time.Time
}
}
}
}
}
Bounds can be added to your types, such as numerical bounds, or non-nil checks. These will only apply to the generated OpenAPI spec for your CRD, and will not be checked in your go or TypeScript types themselves (or in the generated Codecs). As such, the validation of the bounds is only checked on admission by the kubernetes API (via the apiextensions server that manages CRDs).
You can define further, more complex validation and admission control via your operator using admission webhooks, see Admission Control.
The kind
format allows for configuring the additionalPrinterColumns
parameter on a CRD. The format is the same as a CRD, and you add this config as part of "version", next to the schema
:
myKind: {
kind: "MyKind"
current: "v1"
[...]
versions: {
"v1": {
schema: {
spec: {
foo: string
}
}
additionalPrinterColumns: [
{
name: "FOO"
type: "string"
jsonPath: ".spec.foo"
}
]
}
}
}
Example complex schemas used for codegen testing can be found in the cuekind codegen testing directory.