ShortURL: Create k8s API (#108821)
This commit is contained in:
@@ -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 (
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user