Skip to content

Latest commit

 

History

History
328 lines (277 loc) · 13.8 KB

writing-kinds.md

File metadata and controls

328 lines (277 loc) · 13.8 KB

Writing Kinds

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.

Schemas

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

Generating Code

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/

pkg/generated/resource

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 type
  • foo_metadata_gen.go is a file that exists for legacy support, and will be eventually removed from codegen
  • foo_object_gen.go is a file that contains the Foo type, which implements resource.Object. For more information on resource.Object, see Using Kinds or Resource Objects
  • foo_schema_gen.go is a file that contains functions for returning a resource.Kind and resource.Schema (Kind() and Schema() respectively). For more details on resource.Kind, see Using Kinds
  • foo_spec_gen.go is a file that contains a type declaration for the Spec type, as defined in our CUE. It is used by Foo in foo_object_gen.go
  • foo_status_gen.go is a file that contains a type declaration for the Status type, as defined in our CUE. We didn't define a status 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.

plugin/src/generated

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 the Foo interface, which is compatible with the kubernetes API server definition of the Foo kind for that version.
  • types.spec.gen.ts contains the Spec interface, defined by our CUE spec field
  • types.status.gen.ts contains the Status interface, defined by our CUE status 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).

definitions

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.

Toggling Frontend/Backend Codegen

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)

Complex Schemas

Optional Fields

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

Subtypes

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.

Time types

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

Constraints

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.

Custom columns when using kubectl. aka additionalPrinterColumns

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

Examples

Example complex schemas used for codegen testing can be found in the cuekind codegen testing directory.

Recommended Reading