grafana / grafana-app-sdk

An SDK for developing apps for grafana using kubernetes-like storage and operators
Apache License 2.0
45 stars 8 forks source link

App and Runtime Differentiation #391

Closed IfSentient closed 1 month ago

IfSentient commented 1 month ago

This PR is being split into two separate non-draft PR's. This Draft will stay open to show forward context when reviewing other PR's

First PR: PR for Initial App Manifest Implementation

What This PR Does

This PR implements https://github.com/grafana/grafana-app-sdk/issues/385

This PR introduces the app package, which contains a few important concepts:

There is also a "default" app.App implementation as simple.App, and two "runners,": simple.StandaloneOperator, which functions just like a simple.Operator, and plugin.App, which wraps the app in the plugin gRPC runtime, translating the plugin gRPC calls to app ones (this runner is still slightly WIP as there is not a mechanism at the moment for it to get a kube config on initialization).

CUE codegen now also generates a file-based AppManifest (as a CR), and an in-code manifest to use if a AppManifest CRD is not available in the environment. Admission and conversion capabilities can be specified in the apiResource section of the kind's definition like so:

    apiResource: {
        scope: "Namespaced"
        validation: {
            operations: ["create","update"]
        }
        mutation: {
            operations: ["create","update"]
        }
        conversion: true
    }

An update to the tutorial project's cmd/operator/main.go using the new app setup would look like:

const EnvCfgKey = "environment_config"

func main() {
    // Configure the default logger to use slog
    logging.DefaultLogger = logging.NewSLogLogger(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelDebug,
    }))

    //Load the config from the environment
    cfg, err := LoadConfigFromEnv()
    if err != nil {
        logging.DefaultLogger.With("error", err).Error("Unable to load config from environment")
        panic(err)
    }

    // Load the kube config
    kubeConfig, err := LoadInClusterConfig()
    if err != nil {
        logging.DefaultLogger.With("error", err).Error("Unable to load kubernetes configuration")
        panic(err)
    }

    // Set up the operator configuration (kubeconfig, metrics, tracing, etc.)
    // This is the same as the config for an operator in the old way of doing things, but contains the additional AppConfig to pass on when initializing the App
    operatorConfig := simple.OperatorAppConfig{
        AppConfig: app.AppConfig{
            Kubeconfig: kubeConfig.RestConfig,
            ExtraConfig: map[string]any{
                EnvCfgKey: cfg, // Add the config loaded from the env to the app config just in case we need it
            },
        },
        MetricsConfig: simple.MetricsConfig{
            Enabled: true,
        },
        TracingConfig: simple.TracingConfig{
            Enabled: true,
            OpenTelemetryConfig: simple.OpenTelemetryConfig{
                Host:        cfg.OTelConfig.Host,
                Port:        cfg.OTelConfig.Port,
                ConnType:    simple.OTelConnType(cfg.OTelConfig.ConnType),
                ServiceName: cfg.OTelConfig.ServiceName,
            },
        },
        WebhookConfig: simple.OperatorWebhookConfig{
            Port: cfg.WebhookServer.Port,
            TLSConfig: k8s.TLSConfig{
                CertPath: cfg.WebhookServer.TLSCertPath,
                KeyPath:  cfg.WebhookServer.TLSKeyPath,
            },
        },
    }

    operator, err := simple.NewStandaloneOperator(simple.NewAppProvider(generated.LocalManifest(), newApp))
    if err != nil {
        logging.DefaultLogger.With("error", err).Error("Unable to create operator")
        panic(err)
    }

    stopCh := make(chan struct{})

    // Signal channel
    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
    go func() {
        <-sigCh
        stopCh <- struct{}{}
    }()

    err = operator.Run(operatorConfig, stopCh)
    if err != nil {
        logging.DefaultLogger.With("error", err).Error("Operator run error")
        panic(err)
    }
    logging.DefaultLogger.Info("Normal operator exit")
}

// newApp is a function which creates a new app.App from an app.AppConfig
func newApp(cfg app.AppConfig) (app.App, error) {
    watcher, err := watchers.NewIssueWatcher()
    if err != nil {
        return nil, err
    }

    return simple.NewApp(simple.AppConfig{
        Kubeconfig: cfg.Kubeconfig,
        ManagedKinds: []simple.AppManagedKind{{
            Kind:    issue.Kind(),
            Watcher: watcher,
            Validator: validators.NewIssueValidator(),
            Mutator: mutators.NewIssueMutator(),
        }},
    })
}
IfSentient commented 1 month ago

This PR was superseded by https://github.com/grafana/grafana-app-sdk/pull/402