ShortURL: Use UpdateStatus client (#111170)
This commit is contained in:
@@ -30,11 +30,12 @@ var (
|
||||
|
||||
func New(cfg app.Config) (app.App, error) {
|
||||
cfg.KubeConfig.APIPath = "apis"
|
||||
client, err := k8s.NewClientRegistry(cfg.KubeConfig, k8s.DefaultClientConfig()).
|
||||
tmp, err := k8s.NewClientRegistry(cfg.KubeConfig, k8s.DefaultClientConfig()).
|
||||
ClientFor(shorturlv1alpha1.ShortURLKind())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create client")
|
||||
}
|
||||
client := shorturlv1alpha1.NewShortURLClient(tmp)
|
||||
|
||||
simpleConfig := simple.AppConfig{
|
||||
Name: "shorturl",
|
||||
@@ -81,8 +82,8 @@ func New(cfg app.Config) (app.App, error) {
|
||||
Name: req.ResourceIdentifier.Name,
|
||||
}
|
||||
|
||||
info := &shorturlv1alpha1.ShortURL{}
|
||||
if err := client.GetInto(ctx, id, info); err != nil {
|
||||
info, err := client.Get(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -93,7 +94,12 @@ func New(cfg app.Config) (app.App, error) {
|
||||
if err != nil {
|
||||
logging.FromContext(ctx).Warn("unable to create background identity", "err", err)
|
||||
} else {
|
||||
_, _ = client.Update(ctx, id, info, resource.UpdateOptions{})
|
||||
_, err = client.UpdateStatus(ctx, id, info.Status, resource.UpdateOptions{
|
||||
ResourceVersion: info.ResourceVersion,
|
||||
})
|
||||
if err != nil {
|
||||
logging.FromContext(ctx).Warn("unable to update status", "err", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
|
||||
"github.com/grafana/grafana/pkg/services/shorturls"
|
||||
@@ -31,7 +32,7 @@ func TestShortURLAPIEndpoint(t *testing.T) {
|
||||
Path: cmd.Path,
|
||||
}
|
||||
service := &fakeShortURLService{
|
||||
createShortURLFunc: func(ctx context.Context, user *user.SignedInUser, cmd *dtos.CreateShortURLCmd) (*shorturls.ShortUrl, error) {
|
||||
createShortURLFunc: func(ctx context.Context, user identity.Requester, cmd *dtos.CreateShortURLCmd) (*shorturls.ShortUrl, error) {
|
||||
return createResp, nil
|
||||
},
|
||||
createConvertShortURLToDTO: func(shortURL *shorturls.ShortUrl, appURL string) *dtos.ShortURL {
|
||||
@@ -81,7 +82,7 @@ func createShortURLScenario(t *testing.T, desc string, url string, routePattern
|
||||
}
|
||||
|
||||
type fakeShortURLService struct {
|
||||
createShortURLFunc func(ctx context.Context, user *user.SignedInUser, cmd *dtos.CreateShortURLCmd) (*shorturls.ShortUrl, error)
|
||||
createShortURLFunc func(ctx context.Context, user identity.Requester, cmd *dtos.CreateShortURLCmd) (*shorturls.ShortUrl, error)
|
||||
createConvertShortURLToDTO func(shortURL *shorturls.ShortUrl, appURL string) *dtos.ShortURL
|
||||
}
|
||||
|
||||
@@ -89,11 +90,11 @@ func (s *fakeShortURLService) List(ctx context.Context, orgID int64) ([]*shortur
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *fakeShortURLService) GetShortURLByUID(ctx context.Context, user *user.SignedInUser, uid string) (*shorturls.ShortUrl, error) {
|
||||
func (s *fakeShortURLService) GetShortURLByUID(ctx context.Context, user identity.Requester, uid string) (*shorturls.ShortUrl, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s *fakeShortURLService) CreateShortURL(ctx context.Context, user *user.SignedInUser, cmd *dtos.CreateShortURLCmd) (*shorturls.ShortUrl, error) {
|
||||
func (s *fakeShortURLService) CreateShortURL(ctx context.Context, user identity.Requester, cmd *dtos.CreateShortURLCmd) (*shorturls.ShortUrl, error) {
|
||||
if s.createShortURLFunc != nil {
|
||||
return s.createShortURLFunc(ctx, user, cmd)
|
||||
}
|
||||
|
||||
@@ -21,10 +21,17 @@ func convertToK8sResource(v *shorturls.ShortUrl, namespacer request.NamespaceMap
|
||||
status := shorturl.ShortURLStatus{
|
||||
LastSeenAt: v.LastSeenAt,
|
||||
}
|
||||
|
||||
// resourceVersion can't be 0, since we are using the lastSeenAt value, when it's zero we default to current time
|
||||
resourceVersion := fmt.Sprintf("%d", v.LastSeenAt)
|
||||
if v.LastSeenAt == 0 {
|
||||
resourceVersion = fmt.Sprintf("%d", time.Now().UnixMilli())
|
||||
}
|
||||
|
||||
p := &shorturl.ShortURL{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: v.Uid,
|
||||
ResourceVersion: fmt.Sprintf("%d", v.LastSeenAt),
|
||||
ResourceVersion: resourceVersion,
|
||||
CreationTimestamp: metav1.NewTime(time.UnixMilli(v.CreatedAt)),
|
||||
Namespace: namespacer(v.OrgId),
|
||||
},
|
||||
|
||||
@@ -16,9 +16,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"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 (
|
||||
@@ -87,13 +85,7 @@ func (s *legacyStorage) Get(ctx context.Context, name string, options *metav1.Ge
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert any identity.Requester to *user.SignedInUser
|
||||
signedInUser, err := convertRequesterToSignedInUser(requester)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert requester: %w", err)
|
||||
}
|
||||
|
||||
dto, err := s.service.GetShortURLByUID(ctx, signedInUser, name)
|
||||
dto, err := s.service.GetShortURLByUID(ctx, requester, name)
|
||||
if err != nil || dto == nil {
|
||||
if errors.Is(err, shorturls.ErrShortURLNotFound) || err == nil {
|
||||
err = k8serrors.NewNotFound(shorturl.ShortURLKind().GroupVersionResource().GroupResource(), name)
|
||||
@@ -114,12 +106,6 @@ func (s *legacyStorage) Create(ctx context.Context,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Convert any identity.Requester to *user.SignedInUser
|
||||
signedInUser, err := convertRequesterToSignedInUser(requester)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert requester: %w", err)
|
||||
}
|
||||
|
||||
if createValidation != nil {
|
||||
if err := createValidation(ctx, obj.DeepCopyObject()); err != nil {
|
||||
return nil, err
|
||||
@@ -133,7 +119,7 @@ func (s *legacyStorage) Create(ctx context.Context,
|
||||
Path: p.Spec.Path,
|
||||
UID: p.Name,
|
||||
}
|
||||
out, err := s.service.CreateShortURL(ctx, signedInUser, cmd)
|
||||
out, err := s.service.CreateShortURL(ctx, requester, cmd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -154,13 +140,7 @@ func (s *legacyStorage) Update(ctx context.Context,
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// Convert any identity.Requester to *user.SignedInUser
|
||||
signedInUser, err := convertRequesterToSignedInUser(requester)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to convert requester: %w", err)
|
||||
}
|
||||
|
||||
shortURL, err := s.service.GetShortURLByUID(ctx, signedInUser, name)
|
||||
shortURL, err := s.service.GetShortURLByUID(ctx, requester, name)
|
||||
if err != nil || shortURL == nil {
|
||||
if errors.Is(err, shorturls.ErrShortURLNotFound) || err == nil {
|
||||
err = k8serrors.NewNotFound(shorturl.ShortURLKind().GroupVersionResource().GroupResource(), name)
|
||||
@@ -173,7 +153,7 @@ func (s *legacyStorage) Update(ctx context.Context,
|
||||
return nil, false, err
|
||||
}
|
||||
// Fetch the updated short URL to return
|
||||
updatedLegacyShortURL, err := s.service.GetShortURLByUID(ctx, signedInUser, name)
|
||||
updatedLegacyShortURL, err := s.service.GetShortURLByUID(ctx, requester, name)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@@ -199,27 +179,3 @@ func (s *legacyStorage) Delete(ctx context.Context, name string, deleteValidatio
|
||||
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")
|
||||
}
|
||||
|
||||
// convertRequesterToSignedInUser converts any identity.Requester to *user.SignedInUser
|
||||
// This is needed because some legacy shorturls service methods still expect SignedInUser
|
||||
func convertRequesterToSignedInUser(requester identity.Requester) (*user.SignedInUser, error) {
|
||||
// If it's already a SignedInUser, return it directly
|
||||
if signedInUser, ok := requester.(*user.SignedInUser); ok {
|
||||
return signedInUser, nil
|
||||
}
|
||||
|
||||
// If it's a StaticRequester (service identity), convert it
|
||||
if staticRequester, ok := requester.(*identity.StaticRequester); ok {
|
||||
return &user.SignedInUser{
|
||||
UserID: staticRequester.UserID, // Used for CreatedBy field
|
||||
OrgID: staticRequester.OrgID, // Used in SQL queries
|
||||
}, nil
|
||||
}
|
||||
|
||||
// If it's an authn.Identity, use its SignedInUser method
|
||||
if authnIdentity, ok := requester.(*authn.Identity); ok {
|
||||
return authnIdentity.SignedInUser(), nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("unsupported identity type")
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
restclient "k8s.io/client-go/rest"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/app"
|
||||
@@ -86,3 +87,18 @@ func (s *ShortURLAppInstaller) GetLegacyStorage(requested schema.GroupVersionRes
|
||||
)
|
||||
return legacyStore
|
||||
}
|
||||
|
||||
func (s *ShortURLAppInstaller) GetLegacyStatus(requested schema.GroupVersionResource, unified *appsdkapiserver.StatusREST) rest.Storage {
|
||||
gvr := shorturl.ShortURLKind().GroupVersionResource()
|
||||
if requested.String() != gvr.String() {
|
||||
return nil
|
||||
}
|
||||
return &statusDualWriter{
|
||||
gv: gvr.GroupVersion(),
|
||||
status: unified,
|
||||
legacy: &legacyStorage{
|
||||
service: s.service,
|
||||
namespacer: s.namespacer,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
package shorturl
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
"sigs.k8s.io/structured-merge-diff/v6/fieldpath"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/k8s/apiserver"
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
shorturl "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/services/shorturls"
|
||||
)
|
||||
|
||||
type statusDualWriter struct {
|
||||
gv schema.GroupVersion
|
||||
status *apiserver.StatusREST
|
||||
legacy *legacyStorage
|
||||
}
|
||||
|
||||
var (
|
||||
_ rest.Patcher = (*statusDualWriter)(nil)
|
||||
_ rest.Storage = (*statusDualWriter)(nil)
|
||||
_ rest.ResetFieldsStrategy = (*statusDualWriter)(nil)
|
||||
)
|
||||
|
||||
// Destroy implements rest.Storage.
|
||||
func (s *statusDualWriter) Destroy() {}
|
||||
|
||||
// New implements rest.Storage.
|
||||
func (s *statusDualWriter) New() runtime.Object {
|
||||
return s.legacy.New()
|
||||
}
|
||||
|
||||
// Get implements rest.Patcher.
|
||||
func (s *statusDualWriter) Get(ctx context.Context, name string, options *v1.GetOptions) (runtime.Object, error) {
|
||||
return s.legacy.Get(ctx, name, options)
|
||||
}
|
||||
|
||||
// Update implements rest.Patcher.
|
||||
func (s *statusDualWriter) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *v1.UpdateOptions) (runtime.Object, bool, error) {
|
||||
requester, err := identity.GetRequester(ctx)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
shortURL, err := s.legacy.service.GetShortURLByUID(ctx, requester, name)
|
||||
if err != nil || shortURL == nil {
|
||||
if errors.Is(err, shorturls.ErrShortURLNotFound) || err == nil {
|
||||
err = k8serrors.NewNotFound(shorturl.ShortURLKind().GroupVersionResource().GroupResource(), name)
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
// This ignores the incoming and updates it directly
|
||||
err = s.legacy.service.UpdateLastSeenAt(ctx, shortURL)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
getter := func(getter rest.Getter) (*shorturl.ShortURL, error) {
|
||||
obj, err := getter.Get(ctx, name, &v1.GetOptions{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
val, ok := obj.(*shorturl.ShortURL)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("expected ShortURL but got %T", obj)
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
legacy, err := getter(s.legacy)
|
||||
if err != nil {
|
||||
return nil, false, err // unable to get legacy object
|
||||
}
|
||||
|
||||
unified, err := getter(s.status)
|
||||
if err != nil {
|
||||
logging.FromContext(ctx).Warn("unable to read unified status", "error", err)
|
||||
return legacy, false, nil
|
||||
}
|
||||
|
||||
// Use the same status from legacy in unified
|
||||
unified.Status = legacy.Status
|
||||
|
||||
_, _, err = s.status.Update(ctx, name, rest.DefaultUpdatedObjectInfo(unified), createValidation, updateValidation, false, options)
|
||||
if err != nil {
|
||||
logging.FromContext(ctx).Warn("error updating unified status", "error", err)
|
||||
}
|
||||
|
||||
return legacy, false, err
|
||||
}
|
||||
|
||||
// GetResetFields implements rest.ResetFieldsStrategy
|
||||
func (s *statusDualWriter) GetResetFields() map[fieldpath.APIVersion]*fieldpath.Set {
|
||||
fields := map[fieldpath.APIVersion]*fieldpath.Set{
|
||||
fieldpath.APIVersion(s.gv.String()): fieldpath.NewSet(
|
||||
fieldpath.MakePathOrDie("spec"),
|
||||
fieldpath.MakePathOrDie("metadata"),
|
||||
),
|
||||
}
|
||||
return fields
|
||||
}
|
||||
@@ -12,18 +12,18 @@ import (
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/registry/generic"
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
genericapiserver "k8s.io/apiserver/pkg/server"
|
||||
serverstore "k8s.io/apiserver/pkg/server/storage"
|
||||
"k8s.io/kube-openapi/pkg/common"
|
||||
|
||||
appsdkapiserver "github.com/grafana/grafana-app-sdk/k8s/apiserver"
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
|
||||
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"
|
||||
"github.com/grafana/grafana/pkg/storage/legacysql/dualwrite"
|
||||
)
|
||||
|
||||
type LegacyStorageGetterFunc func(schema.GroupVersionResource) grafanarest.Storage
|
||||
@@ -32,6 +32,12 @@ type LegacyStorageProvider interface {
|
||||
GetLegacyStorage(schema.GroupVersionResource) grafanarest.Storage
|
||||
}
|
||||
|
||||
// In the rare case that that legacy needs to support the status subresource
|
||||
// Unlike resource storage, dual writing must be managed explicitly
|
||||
type LegacyStatusProvider interface {
|
||||
GetLegacyStatus(schema.GroupVersionResource, *appsdkapiserver.StatusREST) rest.Storage
|
||||
}
|
||||
|
||||
type AuthorizerProvider interface {
|
||||
GetAuthorizer() authorizer.Authorizer
|
||||
}
|
||||
|
||||
@@ -60,25 +60,41 @@ func (s *serverWrapper) InstallAPIGroup(apiGroupInfo *genericapiserver.APIGroupI
|
||||
continue
|
||||
}
|
||||
storage := s.configureStorage(gr, dualWriteSupported, restStorage)
|
||||
if unifiedStorage, ok := storage.(grafanarest.Storage); ok && dualWriteSupported {
|
||||
log.Debug("Configuring dual writer for storage", "resource", gr.String(), "version", v, "storagePath", storagePath)
|
||||
dw, err := NewDualWriter(
|
||||
s.ctx,
|
||||
gr,
|
||||
s.storageOpts,
|
||||
legacyProvider.GetLegacyStorage(gr.WithVersion(v)),
|
||||
unifiedStorage,
|
||||
s.kvStore,
|
||||
s.lock,
|
||||
s.namespaceMapper,
|
||||
s.dualWriteService,
|
||||
s.dualWriterMetrics,
|
||||
s.builderMetrics,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
if dualWriteSupported {
|
||||
if unifiedStorage, ok := storage.(grafanarest.Storage); ok {
|
||||
log.Debug("Configuring dual writer for storage", "resource", gr.String(), "version", v, "storagePath", storagePath)
|
||||
storage, err = NewDualWriter(
|
||||
s.ctx,
|
||||
gr,
|
||||
s.storageOpts,
|
||||
legacyProvider.GetLegacyStorage(gr.WithVersion(v)),
|
||||
unifiedStorage,
|
||||
s.kvStore,
|
||||
s.lock,
|
||||
s.namespaceMapper,
|
||||
s.dualWriteService,
|
||||
s.dualWriterMetrics,
|
||||
s.builderMetrics,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if statusRest, ok := storage.(*appsdkapiserver.StatusREST); ok {
|
||||
parentPath := strings.TrimSuffix(storagePath, "/status")
|
||||
parentStore, ok := apiGroupInfo.VersionedResourcesStorageMap[v][parentPath]
|
||||
if ok {
|
||||
if _, isMode4or5 := parentStore.(*genericregistry.Store); !isMode4or5 {
|
||||
// When legacy resources have status, the dual writing must be handled explicitly
|
||||
if statusProvider, ok := s.installer.(LegacyStatusProvider); ok {
|
||||
storage = statusProvider.GetLegacyStatus(gr.WithVersion(v), statusRest)
|
||||
} else {
|
||||
log.Warn("skipped registering status sub-resource that does not support dual writing",
|
||||
"resource", gr.String(), "version", v, "storagePath", storagePath)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
storage = dw
|
||||
}
|
||||
apiGroupInfo.VersionedResourcesStorageMap[v][storagePath] = storage
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
GetShortURLByUID(ctx context.Context, user *user.SignedInUser, uid string) (*ShortUrl, error)
|
||||
CreateShortURL(ctx context.Context, user *user.SignedInUser, cmd *dtos.CreateShortURLCmd) (*ShortUrl, error)
|
||||
GetShortURLByUID(ctx context.Context, user identity.Requester, uid string) (*ShortUrl, error)
|
||||
CreateShortURL(ctx context.Context, user identity.Requester, cmd *dtos.CreateShortURLCmd) (*ShortUrl, error)
|
||||
UpdateLastSeenAt(ctx context.Context, shortURL *ShortUrl) error
|
||||
DeleteStaleShortURLs(ctx context.Context, cmd *DeleteShortUrlCommand) error
|
||||
ConvertShortURLToDTO(shortURL *ShortUrl, appURL string) *dtos.ShortURL
|
||||
|
||||
@@ -8,9 +8,9 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/shorturls"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
@@ -28,7 +28,7 @@ func ProvideService(db db.DB) *ShortURLService {
|
||||
}
|
||||
}
|
||||
|
||||
func (s ShortURLService) GetShortURLByUID(ctx context.Context, user *user.SignedInUser, uid string) (*shorturls.ShortUrl, error) {
|
||||
func (s ShortURLService) GetShortURLByUID(ctx context.Context, user identity.Requester, uid string) (*shorturls.ShortUrl, error) {
|
||||
return s.SQLStore.Get(ctx, user, uid)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func (s ShortURLService) List(ctx context.Context, orgID int64) ([]*shorturls.Sh
|
||||
return s.SQLStore.List(ctx, orgID)
|
||||
}
|
||||
|
||||
func (s ShortURLService) CreateShortURL(ctx context.Context, user *user.SignedInUser, cmd *dtos.CreateShortURLCmd) (*shorturls.ShortUrl, error) {
|
||||
func (s ShortURLService) CreateShortURL(ctx context.Context, user identity.Requester, cmd *dtos.CreateShortURLCmd) (*shorturls.ShortUrl, error) {
|
||||
relPath := strings.TrimSpace(cmd.Path)
|
||||
|
||||
if path.IsAbs(relPath) {
|
||||
@@ -74,12 +74,12 @@ func (s ShortURLService) CreateShortURL(ctx context.Context, user *user.SignedIn
|
||||
|
||||
now := time.Now().Unix()
|
||||
shortURL := shorturls.ShortUrl{
|
||||
OrgId: user.OrgID,
|
||||
OrgId: user.GetOrgID(),
|
||||
Uid: uid,
|
||||
Path: relPath,
|
||||
CreatedBy: user.UserID,
|
||||
CreatedAt: now,
|
||||
}
|
||||
shortURL.CreatedBy, _ = user.GetInternalID()
|
||||
|
||||
if err := s.SQLStore.Insert(ctx, &shortURL); err != nil {
|
||||
return nil, shorturls.ErrShortURLInternal.Errorf("failed to insert shorturl: %w", err)
|
||||
|
||||
@@ -3,13 +3,13 @@ package shorturlimpl
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/infra/db"
|
||||
"github.com/grafana/grafana/pkg/services/shorturls"
|
||||
"github.com/grafana/grafana/pkg/services/user"
|
||||
)
|
||||
|
||||
type store interface {
|
||||
Get(ctx context.Context, user *user.SignedInUser, uid string) (*shorturls.ShortUrl, error)
|
||||
Get(ctx context.Context, user identity.Requester, uid string) (*shorturls.ShortUrl, error)
|
||||
Update(ctx context.Context, shortURL *shorturls.ShortUrl) error
|
||||
Insert(ctx context.Context, shortURL *shorturls.ShortUrl) error
|
||||
Delete(ctx context.Context, cmd *shorturls.DeleteShortUrlCommand) error
|
||||
@@ -20,10 +20,10 @@ type sqlStore struct {
|
||||
db db.DB
|
||||
}
|
||||
|
||||
func (s sqlStore) Get(ctx context.Context, user *user.SignedInUser, uid string) (*shorturls.ShortUrl, error) {
|
||||
func (s sqlStore) Get(ctx context.Context, user identity.Requester, uid string) (*shorturls.ShortUrl, error) {
|
||||
var shortURL shorturls.ShortUrl
|
||||
err := s.db.WithDbSession(ctx, func(dbSession *db.Session) error {
|
||||
exists, err := dbSession.Where("org_id=? AND uid=?", user.OrgID, uid).Get(&shortURL)
|
||||
exists, err := dbSession.Where("org_id=? AND uid=?", user.GetOrgID(), uid).Get(&shortURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -5,13 +5,14 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
shorturlV1 "github.com/grafana/grafana/apps/shorturl/pkg/apis/shorturl/v1alpha1"
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/options"
|
||||
@@ -28,11 +29,7 @@ func TestMain(m *testing.M) {
|
||||
testsuite.Run(m)
|
||||
}
|
||||
|
||||
var gvr = schema.GroupVersionResource{
|
||||
Group: "shorturl.grafana.app",
|
||||
Version: "v1alpha1",
|
||||
Resource: "shorturls",
|
||||
}
|
||||
var gvr = shorturlV1.ShortURLKind().GroupVersionResource()
|
||||
|
||||
var RESOURCEGROUP = gvr.GroupResource().String()
|
||||
|
||||
@@ -78,29 +75,31 @@ func TestIntegrationShortURL(t *testing.T) {
|
||||
doLegacyOnlyTests(t, helper)
|
||||
})
|
||||
|
||||
for _, mode := range []grafanarest.DualWriterMode{
|
||||
grafanarest.Mode1,
|
||||
grafanarest.Mode2,
|
||||
// grafanarest.Mode3, TODO: the /goto function needs to use an UpdateStatus client
|
||||
// grafanarest.Mode4,
|
||||
} {
|
||||
t.Run(fmt.Sprintf("with dual write (unified storage, mode %d)", mode), func(t *testing.T) {
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
AppModeProduction: false,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeUnified,
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagKubernetesShortURLs,
|
||||
},
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
RESOURCEGROUP: {
|
||||
DualWriterMode: mode,
|
||||
t.Run("modes", func(t *testing.T) {
|
||||
for _, mode := range []grafanarest.DualWriterMode{
|
||||
grafanarest.Mode1,
|
||||
grafanarest.Mode2,
|
||||
grafanarest.Mode3,
|
||||
grafanarest.Mode4,
|
||||
} {
|
||||
t.Run(fmt.Sprintf("dual write (unified storage, mode %d)", mode), func(t *testing.T) {
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
AppModeProduction: false,
|
||||
DisableAnonymous: true,
|
||||
APIServerStorageType: options.StorageTypeUnified,
|
||||
EnableFeatureToggles: []string{
|
||||
featuremgmt.FlagKubernetesShortURLs,
|
||||
},
|
||||
},
|
||||
UnifiedStorageConfig: map[string]setting.UnifiedStorageConfig{
|
||||
RESOURCEGROUP: {
|
||||
DualWriterMode: mode,
|
||||
},
|
||||
},
|
||||
})
|
||||
doDualWriteTests(t, helper, mode)
|
||||
})
|
||||
doDualWriteTests(t, helper, mode)
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("with dual write (unified storage, mode 5)", func(t *testing.T) {
|
||||
helper := apis.NewK8sTestHelper(t, testinfra.GrafanaOpts{
|
||||
@@ -173,7 +172,7 @@ func doLegacyOnlyTests(t *testing.T, helper *apis.K8sTestHelper) {
|
||||
User: client.Args.User,
|
||||
Method: http.MethodGet,
|
||||
Path: "/goto/" + uid + "?orgId=default",
|
||||
}, (*interface{})(nil))
|
||||
}, (*any)(nil))
|
||||
assert.Equal(t, 302, redirectResponse.Response.StatusCode)
|
||||
})
|
||||
}
|
||||
@@ -278,21 +277,23 @@ func doDualWriteTests(t *testing.T, helper *apis.K8sTestHelper, mode grafanarest
|
||||
User: client.Args.User,
|
||||
Method: http.MethodGet,
|
||||
Path: "/goto/" + uid + "?orgId=default",
|
||||
}, (*interface{})(nil))
|
||||
}, (*any)(nil))
|
||||
assert.Equal(t, 302, redirectResponse.Response.StatusCode)
|
||||
|
||||
// Verify lastSeenAt was updated (should be > 0 now)
|
||||
found, err := client.Resource.Get(context.Background(), uid, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
status, exists := found.Object["status"].(map[string]interface{})
|
||||
assert.True(t, exists)
|
||||
lastSeenAt, exists := status["lastSeenAt"].(int64)
|
||||
assert.True(t, exists)
|
||||
require.EventuallyWithT(t, func(t *assert.CollectT) {
|
||||
// Verify lastSeenAt was updated (should be > 0 now)
|
||||
found, err := client.Resource.Get(context.Background(), uid, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Greater(t, lastSeenAt, int64(0))
|
||||
lastSeenAt, exists, err := unstructured.NestedInt64(found.Object, "status", "lastSeenAt")
|
||||
require.NoError(t, err)
|
||||
require.True(t, exists)
|
||||
|
||||
require.Greater(t, lastSeenAt, int64(1), "lastSeenAt should be greater than 1 after redirect")
|
||||
}, time.Second*5, time.Millisecond*75, "lastSeenAt should be updated after redirect")
|
||||
|
||||
// Clean up
|
||||
err = client.Resource.Delete(context.Background(), uid, metav1.DeleteOptions{})
|
||||
err := client.Resource.Delete(context.Background(), uid, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
@@ -458,7 +459,7 @@ func doUnifiedOnlyTests(t *testing.T, helper *apis.K8sTestHelper) {
|
||||
User: client.Args.User,
|
||||
Method: http.MethodGet,
|
||||
Path: "/goto/" + uid + "?orgId=default",
|
||||
}, (*interface{})(nil))
|
||||
}, (*any)(nil))
|
||||
assert.Equal(t, 302, redirectResponse.Response.StatusCode)
|
||||
|
||||
// Clean up
|
||||
@@ -505,9 +506,9 @@ func getFromBothAPIs(t *testing.T,
|
||||
|
||||
if legacyShortURL != nil {
|
||||
// If legacy API returns data, verify consistency
|
||||
spec, ok := k8sResource.Object["spec"].(map[string]interface{})
|
||||
spec, ok := k8sResource.Object["spec"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
status, ok := k8sResource.Object["status"].(map[string]interface{})
|
||||
status, ok := k8sResource.Object["status"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, legacyShortURL.Uid, k8sResource.GetName())
|
||||
assert.Equal(t, legacyShortURL.Path, spec["path"].(string))
|
||||
|
||||
Reference in New Issue
Block a user