Within the context of the grafana-app-sdk
, an App is a collection of behaviors which can be run by a runner, typically as a kubernetes operator.
An App and the way which it is run are decoupled, such that an App can be run in any supported runner format.
Currently, the only out-of-the-box support in the grafana-app-sdk
is for running an App as a standalone kubernetes operator,
but additional runners are expected to be added in the future.
To quickly get app code to look at, either consider running through the tutorial, or run the following commands in a directory you want to build your app in:
grafana-app-sdk project init test-app
grafana-app-sdk project kind add MyTestKind
grafana-app-sdk generate
grafana-app-sdk project component add operator
As mentioned, an App is a collection of behaviors, which is defined by the app.App interface:
type App interface {
// Validate validates the incoming request, and returns an error if validation fails
Validate(ctx context.Context, request *AdmissionRequest) error
// Mutate runs mutation on the incoming request, responding with a MutatingResponse on success, or an error on failure
Mutate(ctx context.Context, request *AdmissionRequest) (*MutatingResponse, error)
// Convert converts the object based on the ConversionRequest, returning a RawObject which MUST contain
// the converted bytes and encoding (Raw and Encoding respectively), and MAY contain the Object representation of those bytes.
// It returns an error if the conversion fails, or if the functionality is not supported by the app.
Convert(ctx context.Context, req ConversionRequest) (*RawObject, error)
// CallResourceCustomRoute handles the call to a resource custom route, and returns a response to the request or an error.
// If the route doesn't exist, the implementer MAY return ErrCustomRouteNotFound to signal to the runner,
// or may choose to return a response with a not found status code and custom body.
// It returns an error if the functionality is not supported by the app.
CallResourceCustomRoute(ctx context.Context, request *ResourceCustomRouteRequest) (*ResourceCustomRouteResponse, error)
// ManagedKinds returns a slice of Kinds which are managed by this App.
// If there are multiple versions of a Kind, each one SHOULD be returned by this method,
// as app runners may depend on having access to all kinds.
ManagedKinds() []resource.Kind
// Runner returns a Runnable with an app main loop. Any business logic that is not/can not be exposed
// via other App interfaces should be contained within this method.
// Runnable MAY be nil, in which case, the app has no main loop business logic.
Runner() Runnable
}
An App doesn't need to support all these behaviors, and if it doesn't, it returns app.ErrNotImplemented
from that function.
The App indicates which behaviors it supports to the runner via its Manifest.
All of these behaviors (except Runner()
and ManagedKinds()
) are actions taken on one or more kinds managed by the app.
For example, when a new resource is created for a kind which the app manages, Mutate
and Validate
will both be called if supported.
Convert
may be called if the stored version is different from the version used in the create request.
CallResourceCustomRoute
is currently unsupported in the operator runner (as it is not a supported action for a standalone kubernetes operator),
but is to be used on custom subroutes for resources (unsupported by kubernetes CRDs). This will be further elaborated on when new runners are released.
ManagedKinds()
is used by the runner to get the go types for kinds managed by the app (as the manifest is data and does not contain the go types used).
Finally, Runner()
returns a main loop for the app, for logic which is not explicitly event-based like Validate
, Mutate
, and Convert
.
However, often this main loop should also be event-based, using the operator pattern to watch resources and react to changes.
This behavior is the default behavior of Runner()
in simple.App
.
The app manifest is a collection of data about which kinds are managed by the app, what behaviors are supported by the app for each kind,
and other additional information needed for the app to function (such as additional permissions).
When you write your CUE manifest and use grafana-app-sdk generate
, this manifest will be automatically generated for you,
as both an embeddable go variable, or a JSON/YAML custom resource. Writing a manifest from scratch is not advised, but can be done.
See manifest.cue for the definition of the custom resource, or manifest.go
to write your app.ManifestData
in go directly.
The easiest way to get started on this is to use grafana-app-sdk project init <my-project-module-name>
.
This will create a starting CUE manifest to use with grafana-app-sdk generate
. You can add template kinds to this with
grafana-app-sdk project kind add <MyKindName>
.
The standard way to build an app, without implementing app.App
yourself, is to use the simple
package's App
type.
simple.NewApp
creates a new simple.App
, using the provided simple.AppConfig
.
simple.App
runs an operator as its main Runner()
code, and the config has you specify watchers or reconcilers for
the kinds you manage with the app.
A simple.App
has two variants for kinds which you attach watchers/reconcilers to: AppManagedKinds
and AppUnmanagedKinds
.
AppManagedKinds
are kinds which your app owns and manages, which you can attach mutation, validation, and conversion logic to.
AppUnmanagedKinds
are kinds which your app wants to watch (such as dashboards), but does not own or manage.
The easiest way to see how to use simple.App
is to use grafana-app-sdk project component add operator
, or to
follow the tutorial, which will have you build out a fully-featured simple.App
with a watcher, validation, and mutation.
In order for an app to be run by a runner, you need something which will provide a manifest to the runner, and instantiate a new instance of your app once the runner has loaded the configuration necessary for running it. In this case, this is defined by app.Provider:
// Provider represents a type which can provide an app manifest, and create a new App when given a configuration.
// It should be used by runners to determine an app's capabilities and create an instance of the app to run.
type Provider interface {
// Manifest returns a Manifest, which may contain ManifestData or may point to a location where ManifestData can be fetched from.
// The runner should use the ManifestData to determine app capabilities.
Manifest() Manifest
// SpecificConfig is any app-specific config that cannot be loaded by the runner that should be provided in NewApp
SpecificConfig() SpecificConfig
// NewApp creates a new App instance using the provided config, or returns an error if an App cannot be instantiated.
NewApp(Config) (App, error)
}
Luckily, the simple
package has a simple implementation of this, with simple.AppProvider
.
If you use grafana-app-sdk project component add operator
, this is automatically generated for you.
Finally, we need to run our app. As mentioned previously, the only currently supported runner in the grafana-app-sdk
is a standalone kubernetes operator. However, because the app.App
and app.Provider
interfaces are what are used to run an app,
app runners can be build outside of the grafana-app-sdk
which will run any valid app.
In this case, though, to run as a standalone kubernetes operator, we use the operator.Runner
type
(instantiated with operator.NewRunner
). This runner takes configuration necessary for running as a standalone operator
(kubeconfig, webhook configuration, etc.), and then runs any apps given to its Run
method via their AppProvider
.
As an example, let's build a quick, simple app that runs as a standalone operator, assuming we have a kind already generated. You can quickly generate a kind and manifest to follow this code with:
mkdir test-app && cd test-app && grafana-app-sdk project init test-app && grafana-app-sdk project kind add MyKind && grafana-app-sdk generate
Now, for simplicity, we'll put everything in a main.go
file:
package main
import (
"context"
"log/slog"
"os"
"os/signal"
"github.com/grafana/grafana-app-sdk/app"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/grafana/grafana-app-sdk/operator"
"github.com/grafana/grafana-app-sdk/simple"
"k8s.io/client-go/tools/clientcmd"
"test-app/pkg/generated"
mykind "test-app/pkg/generated/mykind/v1"
)
func main() {
// Configure the default logger to use slog
logging.DefaultLogger = logging.NewSLogLogger(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
// Load the kube config from a file
kubeConfig, err := clientcmd.BuildConfigFromFlags("", "local.kubeconfig")
if err != nil {
panic(err)
}
// Build the runner
runner, err := operator.NewRunner(operator.RunnerConfig{
KubeConfig: *kubeConfig,
})
if err != nil {
panic(err)
}
// Run the app
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
defer cancel()
err = runner.Run(ctx, simple.NewAppProvider(generated.LocalManifest(), nil, NewApp))
}
func NewApp(cfg app.Config) (app.App, error) {
return simple.NewApp(simple.AppConfig{
Name: "my-test-app",
KubeConfig: cfg.KubeConfig,
ManagedKinds: []simple.AppManagedKind{{
Kind: mykind.Kind(),
Reconciler: &operator.TypedReconciler[*mykind.MyKind]{
ReconcileFunc: func(ctx context.Context, req operator.TypedReconcileRequest[*mykind.MyKind]) (operator.ReconcileResult, error) {
logging.FromContext(ctx).Info("Reconcile request", "name", req.Object.GetName(), "action", operator.ResourceActionFromReconcileAction(req.Action))
return operator.ReconcileResult{}, nil
},
},
}},
})
}
You can connect this to a kubernetes API server by changing "local.kubeconfig"
(or populating that file),
and making sure the MyKind
CRD exists by applying the generated definitions/mykind.testapp.ext.grafana.com.json
file.
You can expand on this to add Validation or Mutation (or conversion), but keep in mind you'll need to add a WebhookConfig
to the operator.RunnerConfig
, and set up the webhook configurations in your kubernetes API server.
Unless you're already familiar with how to set up webhooks, the easier way to do this is to
follow the tutorial which sets up validation and mutation in part 8.