Apps: Add Example App to ./apps (#112069)

* [API Server] Add Example App for reference use.

* Remove Printlns.

* Upgrade app-sdk to v0.46.0, update apps to handle breaking changes.

* Only start the reconciler for the example app if the v1alpha1 API version is enabled.

* Some comment doc updates.

* Run make update-workspace

* Set codeowner for /apps/example

* Run make gofmt and make update-workspace

* Run prettier on apps/example/README.md

* Add COPY apps/example to Dockerfile

* Add an authorizer to the example app.

* Fix import ordering.

* Update apps/example/kinds/manifest.cue

Co-authored-by: Owen Diehl <ow.diehl@gmail.com>

* Run make update-workspace

* Re-run make gen-go for enterprise import updates

* Run make update-workspace

---------

Co-authored-by: Owen Diehl <ow.diehl@gmail.com>
This commit is contained in:
Austin Pond
2025-10-27 12:01:10 -04:00
committed by GitHub
co-authored by Owen Diehl
parent d25f5199c7
commit bf65c43783
71 changed files with 3744 additions and 4 deletions
-1
View File
@@ -57,5 +57,4 @@ import (
_ "github.com/grafana/tempo/pkg/traceql"
_ "github.com/grafana/grafana/apps/alerting/alertenrichment/pkg/apis/alertenrichment/v1beta1"
_ "github.com/grafana/grafana/apps/scope/pkg/apis/scope/v0alpha1"
)
+3
View File
@@ -15,6 +15,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications"
"github.com/grafana/grafana/pkg/registry/apps/alerting/rules"
"github.com/grafana/grafana/pkg/registry/apps/correlations"
"github.com/grafana/grafana/pkg/registry/apps/example"
"github.com/grafana/grafana/pkg/registry/apps/investigations"
"github.com/grafana/grafana/pkg/registry/apps/logsdrilldown"
"github.com/grafana/grafana/pkg/registry/apps/playlist"
@@ -38,10 +39,12 @@ func ProvideAppInstallers(
correlationsAppInstaller *correlations.AppInstaller,
alertingNotificationAppInstaller *notifications.AlertingNotificationsAppInstaller,
logsdrilldownAppInstaller *logsdrilldown.LogsDrilldownAppInstaller,
exampleAppInstaller *example.ExampleAppInstaller,
) []appsdkapiserver.AppInstaller {
installers := []appsdkapiserver.AppInstaller{
playlistAppInstaller,
pluginsApplInstaller,
exampleAppInstaller,
}
//nolint:staticcheck // not yet migrated to OpenFeature
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesShortURLs) {
+3 -1
View File
@@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications"
"github.com/grafana/grafana/pkg/registry/apps/alerting/rules"
"github.com/grafana/grafana/pkg/registry/apps/correlations"
"github.com/grafana/grafana/pkg/registry/apps/example"
"github.com/grafana/grafana/pkg/registry/apps/playlist"
"github.com/grafana/grafana/pkg/registry/apps/plugins"
"github.com/grafana/grafana/pkg/services/featuremgmt"
@@ -19,6 +20,7 @@ func TestProvideAppInstallers_Table(t *testing.T) {
rulesInstaller := &rules.AlertingRulesAppInstaller{}
correlationsAppInstaller := &correlations.AppInstaller{}
notificationsAppInstaller := &notifications.AlertingNotificationsAppInstaller{}
exampleAppInstaller := &example.ExampleAppInstaller{}
tests := []struct {
name string
@@ -35,7 +37,7 @@ func TestProvideAppInstallers_Table(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
features := featuremgmt.WithFeatures(tt.flags...)
got := ProvideAppInstallers(features, playlistInstaller, pluginsInstaller, nil, tt.rulesInst, correlationsAppInstaller, notificationsAppInstaller, nil)
got := ProvideAppInstallers(features, playlistInstaller, pluginsInstaller, nil, tt.rulesInst, correlationsAppInstaller, notificationsAppInstaller, nil, exampleAppInstaller)
if tt.expectRulesApp {
require.Contains(t, got, tt.rulesInst)
} else {
+80
View File
@@ -0,0 +1,80 @@
package example
import (
"fmt"
"strings"
"k8s.io/apiserver/pkg/authorization/authorizer"
restclient "k8s.io/client-go/rest"
"github.com/grafana/grafana-app-sdk/app"
appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver"
"github.com/grafana/grafana-app-sdk/simple"
"github.com/grafana/grafana/apps/example/pkg/apis"
"github.com/grafana/grafana/apps/example/pkg/apis/example/v1alpha1"
exampleapp "github.com/grafana/grafana/apps/example/pkg/app"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
)
var (
_ appsdkapiserver.AppInstaller = (*ExampleAppInstaller)(nil)
)
type ExampleAppInstaller struct {
appsdkapiserver.AppInstaller
cfg *setting.Cfg
}
func (e ExampleAppInstaller) GetAuthorizer() authorizer.Authorizer {
return exampleapp.GetAuthorizer()
}
func RegisterAppInstaller(
cfg *setting.Cfg,
features featuremgmt.FeatureToggles,
) (*ExampleAppInstaller, error) {
installer := &ExampleAppInstaller{
cfg: cfg,
}
// Config specific to the app. This can pull from feature flags or setting.Cfg.
specificConfig := &exampleapp.ExampleConfig{
EnableSomeFeature: true,
}
// Set specificConfig.EnableReconciler to true IFF the v1alpha1 API is enabled in the runtime config.
// This is example-app-specific, as the version the reconciler uses is not served by default and must be enabled via an override.
apiserverRuntimeCfg := cfg.SectionWithEnvOverrides("grafana-apiserver").Key("runtime_config").String()
for _, s := range strings.Split(apiserverRuntimeCfg, ",") {
if len(s) == 0 {
continue
}
arr := strings.SplitN(s, "=", 2)
if len(arr) == 2 {
if arr[0] == fmt.Sprintf("%s/%s", v1alpha1.APIGroup, v1alpha1.APIVersion) {
specificConfig.EnableReconciler = strings.EqualFold("true", arr[1])
break
}
}
}
// Provider is the app provider, which contains the AppManifest, app-specific-config, and the New function for the app
provider := simple.NewAppProvider(apis.LocalManifest(), specificConfig, exampleapp.New)
// appConfig is used alongside the provider for registrion.
// Most of the data is redunant, this may be more optimized in the future.
appConfig := app.Config{
KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method
ManifestData: *apis.LocalManifest().ManifestData,
SpecificConfig: specificConfig,
}
// NewDefaultInstaller gets us the installer we need to underly the ExampleAppInstaller type.
// It does all the hard work of installing our app to the grafana API server
i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appConfig, apis.NewGoTypeAssociator())
if err != nil {
return nil, err
}
installer.AppInstaller = i
return installer, nil
}
+2
View File
@@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications"
"github.com/grafana/grafana/pkg/registry/apps/alerting/rules"
"github.com/grafana/grafana/pkg/registry/apps/correlations"
"github.com/grafana/grafana/pkg/registry/apps/example"
"github.com/grafana/grafana/pkg/registry/apps/investigations"
"github.com/grafana/grafana/pkg/registry/apps/logsdrilldown"
"github.com/grafana/grafana/pkg/registry/apps/playlist"
@@ -26,4 +27,5 @@ var WireSet = wire.NewSet(
rules.RegisterAppInstaller,
notifications.RegisterAppInstaller,
logsdrilldown.RegisterAppInstaller,
example.RegisterAppInstaller,
)
+11 -2
View File
@@ -81,6 +81,7 @@ import (
notifications2 "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications"
"github.com/grafana/grafana/pkg/registry/apps/alerting/rules"
correlations2 "github.com/grafana/grafana/pkg/registry/apps/correlations"
"github.com/grafana/grafana/pkg/registry/apps/example"
"github.com/grafana/grafana/pkg/registry/apps/investigations"
"github.com/grafana/grafana/pkg/registry/apps/logsdrilldown"
"github.com/grafana/grafana/pkg/registry/apps/playlist"
@@ -783,7 +784,11 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
if err != nil {
return nil, err
}
v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, pluginsAppInstaller, shortURLAppInstaller, alertingRulesAppInstaller, appInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller)
exampleAppInstaller, err := example.RegisterAppInstaller(cfg, featureToggles)
if err != nil {
return nil, err
}
v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, pluginsAppInstaller, shortURLAppInstaller, alertingRulesAppInstaller, appInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, exampleAppInstaller)
builderMetrics := builder.ProvideBuilderMetrics(registerer)
apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics)
if err != nil {
@@ -1400,7 +1405,11 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
if err != nil {
return nil, err
}
v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, pluginsAppInstaller, shortURLAppInstaller, alertingRulesAppInstaller, appInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller)
exampleAppInstaller, err := example.RegisterAppInstaller(cfg, featureToggles)
if err != nil {
return nil, err
}
v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, pluginsAppInstaller, shortURLAppInstaller, alertingRulesAppInstaller, appInstaller, alertingNotificationsAppInstaller, logsDrilldownAppInstaller, exampleAppInstaller)
builderMetrics := builder.ProvideBuilderMetrics(registerer)
apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, inlineSecureValueSupport, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics)
if err != nil {
@@ -116,6 +116,13 @@ func (s *serverWrapper) configureStorage(gr schema.GroupResource, dualWriteSuppo
return statusStore
}
// if the storage is a subresource store, we need to extract the underlying generic registry store
if subresourceStore, ok := storage.(*appsdkapiserver.SubresourceREST); ok {
subresourceStore.Store.KeyFunc = grafanaregistry.NamespaceKeyFunc(gr)
subresourceStore.Store.KeyRootFunc = grafanaregistry.KeyRootFunc(gr)
return subresourceStore
}
return storage
}