ShortURL: Create k8s API (#108821)

This commit is contained in:
Ezequiel Victorero
2025-08-04 09:12:12 -03:00
committed by GitHub
parent 3a84f6ad42
commit e88b54e9d3
34 changed files with 1510 additions and 6 deletions
+8 -1
View File
@@ -14,6 +14,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications"
"github.com/grafana/grafana/pkg/registry/apps/investigations"
"github.com/grafana/grafana/pkg/registry/apps/playlist"
"github.com/grafana/grafana/pkg/registry/apps/shorturl"
"github.com/grafana/grafana/pkg/services/apiserver"
"github.com/grafana/grafana/pkg/services/apiserver/builder"
"github.com/grafana/grafana/pkg/services/apiserver/builder/runner"
@@ -24,9 +25,15 @@ import (
// ProvideAppInstallers returns a list of app installers that can be used to install apps.
// This is the pattern that should be used to provide app installers in the app registry.
func ProvideAppInstallers(
features featuremgmt.FeatureToggles,
playlistAppInstaller *playlist.PlaylistAppInstaller,
shorturlAppInstaller *shorturl.ShortURLAppInstaller,
) []appsdkapiserver.AppInstaller {
return []appsdkapiserver.AppInstaller{playlistAppInstaller}
installers := []appsdkapiserver.AppInstaller{playlistAppInstaller}
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesShortURLs) {
installers = append(installers, shorturlAppInstaller)
}
return installers
}
var (
+34
View File
@@ -0,0 +1,34 @@
package shorturl
import (
"fmt"
"time"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
shorturl "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1alpha1"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/shorturls"
)
func convertToK8sResource(v *shorturls.ShortUrl, namespacer request.NamespaceMapper) *shorturl.ShortURL {
spec := shorturl.ShortURLSpec{
Path: v.Path,
}
status := shorturl.ShortURLStatus{
LastSeenAt: v.LastSeenAt,
}
p := &shorturl.ShortURL{
ObjectMeta: metav1.ObjectMeta{
Name: v.Uid,
UID: types.UID(v.Uid),
ResourceVersion: fmt.Sprintf("%d", v.LastSeenAt),
CreationTimestamp: metav1.NewTime(time.UnixMilli(v.CreatedAt)),
Namespace: namespacer(v.OrgId),
},
Spec: spec,
Status: status,
}
return p
}
@@ -0,0 +1,145 @@
package shorturl
import (
"context"
"errors"
"fmt"
"strings"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/registry/rest"
shorturl "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1alpha1"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/authn"
"github.com/grafana/grafana/pkg/services/shorturls"
"github.com/grafana/grafana/pkg/services/user"
)
var (
_ rest.Scoper = (*legacyStorage)(nil)
_ rest.SingularNameProvider = (*legacyStorage)(nil)
_ rest.Getter = (*legacyStorage)(nil)
_ rest.Storage = (*legacyStorage)(nil)
_ rest.Creater = (*legacyStorage)(nil)
_ rest.Updater = (*legacyStorage)(nil)
_ rest.GracefulDeleter = (*legacyStorage)(nil)
)
type legacyStorage struct {
service shorturls.Service
namespacer request.NamespaceMapper
tableConverter rest.TableConvertor
}
func (s *legacyStorage) New() runtime.Object {
return shorturl.ShortURLKind().ZeroValue()
}
func (s *legacyStorage) Destroy() {}
func (s *legacyStorage) NamespaceScoped() bool {
return true // namespace == org
}
func (s *legacyStorage) GetSingularName() string {
return strings.ToLower(shorturl.ShortURLKind().Kind())
}
func (s *legacyStorage) NewList() runtime.Object {
return shorturl.ShortURLKind().ZeroListValue()
}
func (s *legacyStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
}
func (s *legacyStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
return nil, fmt.Errorf("List for shorturl not implemented")
}
func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
requester, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
// Convert identity.Requester to *user.SignedInUser
var signedInUser *user.SignedInUser
if authnIdentity, ok := requester.(*authn.Identity); ok {
signedInUser = authnIdentity.SignedInUser()
} else if userIdentity, ok := requester.(*user.SignedInUser); ok {
signedInUser = userIdentity
} else {
return nil, fmt.Errorf("unsupported identity type")
}
dto, err := s.service.GetShortURLByUID(ctx, signedInUser, name)
if err != nil || dto == nil {
if errors.Is(err, shorturls.ErrShortURLNotFound) || err == nil {
err = k8serrors.NewNotFound(schema.GroupResource{
Group: shorturl.ShortURLKind().Group(),
Resource: shorturl.ShortURLKind().Plural(),
}, name)
}
return nil, err
}
return convertToK8sResource(dto, s.namespacer), nil
}
func (s *legacyStorage) Create(ctx context.Context,
obj runtime.Object,
createValidation rest.ValidateObjectFunc,
options *metav1.CreateOptions,
) (runtime.Object, error) {
requester, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
// Convert identity.Requester to *user.SignedInUser
var signedInUser *user.SignedInUser
if authnIdentity, ok := requester.(*authn.Identity); ok {
signedInUser = authnIdentity.SignedInUser()
} else if userIdentity, ok := requester.(*user.SignedInUser); ok {
signedInUser = userIdentity
} else {
return nil, fmt.Errorf("unsupported identity type")
}
p, ok := obj.(*shorturl.ShortURL)
if !ok {
return nil, fmt.Errorf("expected shorturl?")
}
out, err := s.service.CreateShortURL(ctx, signedInUser, p.Spec.Path)
if err != nil {
return nil, err
}
return s.Get(ctx, out.Uid, nil)
}
func (s *legacyStorage) Update(ctx context.Context,
name string,
objInfo rest.UpdatedObjectInfo,
createValidation rest.ValidateObjectFunc,
updateValidation rest.ValidateObjectUpdateFunc,
forceAllowCreate bool,
options *metav1.UpdateOptions,
) (runtime.Object, bool, error) {
return nil, false, fmt.Errorf("Update for shorturl not implemented")
}
// GracefulDeleter
func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
return nil, false, fmt.Errorf("Delete for shorturl not implemented")
}
// CollectionDeleter
func (s *legacyStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
return nil, fmt.Errorf("DeleteCollection for shorturl not implemented")
}
+97
View File
@@ -0,0 +1,97 @@
package shorturl
import (
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
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/shorturl/pkg/apis"
shorturl "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1alpha1"
shorturlapp "github.com/grafana/grafana/apps/shorturl/pkg/app"
"github.com/grafana/grafana/pkg/apimachinery/utils"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/apiserver/appinstaller"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/shorturls"
"github.com/grafana/grafana/pkg/setting"
)
var (
_ appsdkapiserver.AppInstaller = (*ShortURLAppInstaller)(nil)
_ appinstaller.LegacyStorageProvider = (*ShortURLAppInstaller)(nil)
)
type ShortURLAppInstaller struct {
appsdkapiserver.AppInstaller
cfg *setting.Cfg
service shorturls.Service
}
func RegisterAppInstaller(
cfg *setting.Cfg,
service shorturls.Service,
) (*ShortURLAppInstaller, error) {
installer := &ShortURLAppInstaller{
cfg: cfg,
service: service,
}
specificConfig := any(&shorturlapp.ShortURLConfig{
AppURL: cfg.AppURL,
})
provider := simple.NewAppProvider(apis.LocalManifest(), specificConfig, shorturlapp.New)
appCfg := app.Config{
KubeConfig: restclient.Config{}, // this will be overridden by the installer's InitializeApp method
ManifestData: *apis.LocalManifest().ManifestData,
SpecificConfig: specificConfig,
}
i, err := appsdkapiserver.NewDefaultAppInstaller(provider, appCfg, apis.ManifestGoTypeAssociator, apis.ManifestCustomRouteResponsesAssociator)
if err != nil {
return nil, err
}
installer.AppInstaller = i
return installer, nil
}
func (s *ShortURLAppInstaller) GetLegacyStorage(requested schema.GroupVersionResource) grafanarest.Storage {
gvr := schema.GroupVersionResource{
Group: shorturl.ShortURLKind().Group(),
Version: shorturl.ShortURLKind().Version(),
Resource: shorturl.ShortURLKind().Plural(),
}
if requested.String() != gvr.String() {
return nil
}
legacyStore := &legacyStorage{
service: s.service,
namespacer: request.GetNamespaceMapper(s.cfg),
}
legacyStore.tableConverter = utils.NewTableConverter(
gvr.GroupResource(),
utils.TableColumns{
Definition: []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name"},
{Name: "Path", Type: "string", Format: "string", Description: "The url path"},
{Name: "Last Seen At", Type: "number"},
},
Reader: func(obj any) ([]interface{}, error) {
m, ok := obj.(*shorturl.ShortURL)
if !ok {
return nil, fmt.Errorf("expected shorturl")
}
return []interface{}{
m.Name,
m.Spec.Path,
m.Status.LastSeenAt,
}, nil
},
},
)
return legacyStore
}
+2
View File
@@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/registry/apps/alerting/notifications"
"github.com/grafana/grafana/pkg/registry/apps/investigations"
"github.com/grafana/grafana/pkg/registry/apps/playlist"
"github.com/grafana/grafana/pkg/registry/apps/shorturl"
)
var WireSet = wire.NewSet(
@@ -16,4 +17,5 @@ var WireSet = wire.NewSet(
investigations.RegisterApp,
advisor.RegisterApp,
notifications.RegisterApp,
shorturl.RegisterAppInstaller,
)
+11 -2
View File
@@ -76,6 +76,7 @@ import (
notifications2 "github.com/grafana/grafana/pkg/registry/apps/alerting/notifications"
"github.com/grafana/grafana/pkg/registry/apps/investigations"
"github.com/grafana/grafana/pkg/registry/apps/playlist"
"github.com/grafana/grafana/pkg/registry/apps/shorturl"
"github.com/grafana/grafana/pkg/registry/backgroundsvcs"
"github.com/grafana/grafana/pkg/registry/usagestatssvcs"
"github.com/grafana/grafana/pkg/services/accesscontrol"
@@ -688,7 +689,11 @@ func Initialize(cfg *setting.Cfg, opts Options, apiOpts api.ServerOptions) (*Ser
if err != nil {
return nil, err
}
v2 := appregistry.ProvideAppInstallers(playlistAppInstaller)
shortURLAppInstaller, err := shorturl.RegisterAppInstaller(cfg, shortURLService)
if err != nil {
return nil, err
}
v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, shortURLAppInstaller)
builderMetrics := builder.ProvideBuilderMetrics(registerer)
apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics)
if err != nil {
@@ -1249,7 +1254,11 @@ func InitializeForTest(t sqlutil.ITestDB, testingT interface {
if err != nil {
return nil, err
}
v2 := appregistry.ProvideAppInstallers(playlistAppInstaller)
shortURLAppInstaller, err := shorturl.RegisterAppInstaller(cfg, shortURLService)
if err != nil {
return nil, err
}
v2 := appregistry.ProvideAppInstallers(featureToggles, playlistAppInstaller, shortURLAppInstaller)
builderMetrics := builder.ProvideBuilderMetrics(registerer)
apiserverService, err := apiserver.ProvideService(cfg, featureToggles, routeRegisterImpl, tracingService, serverLockService, sqlStore, kvStore, middlewareHandler, scopedPluginDatasourceProvider, plugincontextProvider, pluginstoreService, dualwriteService, resourceClient, eventualRestConfigProvider, v, eventualRestConfigProvider, registerer, aggregatorRunner, v2, builderMetrics)
if err != nil {
+7
View File
@@ -476,6 +476,13 @@ var (
Owner: grafanaAppPlatformSquad,
FrontendOnly: true,
},
{
Name: "kubernetesShortURLs",
Description: "Routes short url requests from /api to the /apis endpoint",
Stage: FeatureStageExperimental,
Owner: grafanaAppPlatformSquad,
RequiresRestart: true, // changes the API routing
},
{
Name: "dashboardDisableSchemaValidationV1",
Description: "Disable schema validation for dashboards/v1",
+1
View File
@@ -61,6 +61,7 @@ formatString,GA,@grafana/dataviz-squad,false,false,true
kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,false
kubernetesLibraryPanels,experimental,@grafana/grafana-app-platform-squad,false,true,false
kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,true
kubernetesShortURLs,experimental,@grafana/grafana-app-platform-squad,false,true,false
dashboardDisableSchemaValidationV1,experimental,@grafana/grafana-app-platform-squad,false,false,false
dashboardDisableSchemaValidationV2,experimental,@grafana/grafana-app-platform-squad,false,false,false
dashboardSchemaValidationLogging,experimental,@grafana/grafana-app-platform-squad,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
61 kubernetesSnapshots experimental @grafana/grafana-app-platform-squad false true false
62 kubernetesLibraryPanels experimental @grafana/grafana-app-platform-squad false true false
63 kubernetesDashboards experimental @grafana/grafana-app-platform-squad false false true
64 kubernetesShortURLs experimental @grafana/grafana-app-platform-squad false true false
65 dashboardDisableSchemaValidationV1 experimental @grafana/grafana-app-platform-squad false false false
66 dashboardDisableSchemaValidationV2 experimental @grafana/grafana-app-platform-squad false false false
67 dashboardSchemaValidationLogging experimental @grafana/grafana-app-platform-squad false false false
+4
View File
@@ -255,6 +255,10 @@ const (
// Use the kubernetes API in the frontend for dashboards
FlagKubernetesDashboards = "kubernetesDashboards"
// FlagKubernetesShortURLs
// Routes short url requests from /api to the /apis endpoint
FlagKubernetesShortURLs = "kubernetesShortURLs"
// FlagDashboardDisableSchemaValidationV1
// Disable schema validation for dashboards/v1
FlagDashboardDisableSchemaValidationV1 = "dashboardDisableSchemaValidationV1"
+13
View File
@@ -1782,6 +1782,19 @@
"requiresRestart": true
}
},
{
"metadata": {
"name": "kubernetesShortURLs",
"resourceVersion": "1753722806283",
"creationTimestamp": "2025-07-28T17:13:26Z"
},
"spec": {
"description": "Routes short url requests from /api to the /apis endpoint",
"stage": "experimental",
"codeowner": "@grafana/grafana-app-platform-squad",
"requiresRestart": true
}
},
{
"metadata": {
"name": "kubernetesSnapshots",