Files
grafana/pkg/services/apiserver/appinstaller/installer.go
T

202 lines
6.8 KiB
Go

package appinstaller
import (
"context"
"errors"
"fmt"
"maps"
"time"
appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/generic"
genericapiserver "k8s.io/apiserver/pkg/server"
serverstore "k8s.io/apiserver/pkg/server/storage"
"k8s.io/kube-openapi/pkg/common"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
grafanaapiserveroptions "github.com/grafana/grafana/pkg/services/apiserver/options"
)
type LegacyStorageGetterFunc func(schema.GroupVersionResource) grafanarest.Storage
type LegacyStorageProvider interface {
GetLegacyStorage(schema.GroupVersionResource) grafanarest.Storage
}
type AuthorizerProvider interface {
GetAuthorizer() authorizer.Authorizer
}
type AppInstallerConfig struct {
CustomConfig any
AllowedV0Alpha1Resources []string
}
// serverLock interface defines a lock mechanism for executing actions with a timeout
type serverLock interface {
LockExecuteAndRelease(ctx context.Context, actionName string, maxInterval time.Duration, fn func(ctx context.Context)) error
}
// AddToScheme adds app installer schemas to the runtime scheme
func AddToScheme(
appInstallers []appsdkapiserver.AppInstaller,
scheme *runtime.Scheme,
) ([]schema.GroupVersion, error) {
var additionalGroupVersions []schema.GroupVersion
for _, installer := range appInstallers {
if err := installer.AddToScheme(scheme); err != nil {
return nil, fmt.Errorf("failed to add app installer scheme: %w", err)
}
additionalGroupVersions = append(additionalGroupVersions, installer.GroupVersions()...)
}
return additionalGroupVersions, nil
}
// RegisterAdmission combines the existing admission control from builders.
func RegisterAdmission(
existingAdmission admission.Interface,
appInstallers []appsdkapiserver.AppInstaller,
) (admission.Interface, error) {
controllers := []admission.Interface{}
for _, installer := range appInstallers {
factory := installer.AdmissionPlugin()
if factory == nil {
continue
}
admissionInterface, err := factory(nil)
if err != nil {
return nil, fmt.Errorf("failed to create admission plugin: %w", err)
}
controllers = append(controllers, admissionInterface)
}
if existingAdmission != nil {
controllers = append(controllers, existingAdmission)
}
return admission.NewChainHandler(controllers...), nil
}
type AuthorizerRegistrar interface {
Register(gv schema.GroupVersion, authorizer authorizer.Authorizer)
}
func RegisterAuthorizers(
ctx context.Context,
appInstallers []appsdkapiserver.AppInstaller,
registrar AuthorizerRegistrar,
) {
logger := logging.FromContext(ctx)
for _, installer := range appInstallers {
if authorizerProvider, ok := installer.(AuthorizerProvider); ok {
authorizer := authorizerProvider.GetAuthorizer()
for _, gv := range installer.GroupVersions() {
registrar.Register(gv, authorizer)
logger.Debug("Registered authorizer", "group", gv.Group, "version", gv.Version, "app")
}
}
}
}
func BuildOpenAPIDefGetter(
appInstallers []appsdkapiserver.AppInstaller,
) func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
return func(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
defs := make(map[string]common.OpenAPIDefinition)
maps.Copy(defs, appsdkapiserver.GetCommonOpenAPIDefinitions(ref))
for _, installer := range appInstallers {
maps.Copy(defs, installer.GetOpenAPIDefinitions(ref))
}
return defs
}
}
func InstallAPIs(
ctx context.Context,
appInstallers []appsdkapiserver.AppInstaller,
server *genericapiserver.GenericAPIServer,
restOpsGetter generic.RESTOptionsGetter,
storageOpts *grafanaapiserveroptions.StorageOptions,
kvStore grafanarest.NamespacedKVStore,
lock serverLock,
namespaceMapper request.NamespaceMapper,
dualWriteService dualwrite.Service,
dualWriterMetrics *grafanarest.DualWriterMetrics,
builderMetrics *builder.BuilderMetrics,
apiResourceConfig *serverstore.ResourceConfig,
) error {
logger := logging.FromContext(ctx)
for _, installer := range appInstallers {
logger.Debug("Installing APIs for app installer", "app", installer.ManifestData().AppName)
wrapper := &serverWrapper{
ctx: ctx,
GenericAPIServer: server,
installer: installer,
storageOpts: storageOpts,
restOptionsGetter: restOpsGetter,
kvStore: kvStore,
lock: lock,
namespaceMapper: namespaceMapper,
dualWriteService: dualWriteService,
dualWriterMetrics: dualWriterMetrics,
builderMetrics: builderMetrics,
apiResourceConfig: apiResourceConfig,
}
if err := installer.InstallAPIs(wrapper, restOpsGetter); err != nil {
return fmt.Errorf("failed to install APIs for app %s: %w", installer.ManifestData().AppName, err)
}
logger.Info("Installed APIs for app", "app", installer.ManifestData().AppName)
}
return nil
}
// RegisterPostStartHooks registers individual post start hooks for each app installer
func RegisterPostStartHooks(
appInstallers []appsdkapiserver.AppInstaller,
serverConfig *genericapiserver.RecommendedConfig,
) error {
for _, installer := range appInstallers {
md := installer.ManifestData()
if md == nil {
return fmt.Errorf("app installer has nil manifest data: %T", installer)
}
hook := createPostStartHook(installer)
if err := serverConfig.AddPostStartHook(md.AppName, hook); err != nil {
return fmt.Errorf("failed to register post start hook for app %s: %w", md.AppName, err)
}
}
return nil
}
func createPostStartHook(
installer appsdkapiserver.AppInstaller,
) genericapiserver.PostStartHookFunc {
return func(hookContext genericapiserver.PostStartHookContext) error {
logger := logging.FromContext(hookContext.Context)
logger.Debug("Initializing app", "app", installer.ManifestData().AppName)
if err := installer.InitializeApp(*hookContext.LoopbackClientConfig); err != nil && !errors.Is(err, appsdkapiserver.ErrAppAlreadyInitialized) {
logger.Error("Failed to initialize app", "app", installer.ManifestData().AppName, "error", err)
return fmt.Errorf("failed to initialize app %s: %w", installer.ManifestData().AppName, err)
}
logger.Info("App initialized", "app", installer.ManifestData().AppName)
app, err := installer.App()
if err != nil {
logger.Error("Failed to initialize app", "app", installer.ManifestData().AppName, "error", err)
return fmt.Errorf("failed to get app from installer %s: %w", installer.ManifestData().AppName, err)
}
return app.Runner().Run(hookContext.Context)
}
}