ShortURL: Use UpdateStatus client (#111170)

This commit is contained in:
Ryan McKinley
2025-10-30 15:57:03 +03:00
committed by GitHub
parent f185377c68
commit d303746ff9
12 changed files with 250 additions and 130 deletions
+10 -4
View File
@@ -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)
}
}
}()
+5 -4
View File
@@ -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)
}
+8 -1
View File
@@ -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),
},
+4 -48
View File
@@ -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")
}
+16
View File
@@ -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,
},
}
}
+111
View File
@@ -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
}
+34 -18
View File
@@ -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
}
+3 -3
View File
@@ -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)
+4 -4
View File
@@ -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
}
+42 -41
View File
@@ -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))