fix(unified-storage): unlist the deleted resource's versions (#102588)

This commit is contained in:
Mustafa Sencer Özcan
2025-03-27 18:34:37 +01:00
committed by GitHub
parent d10fdc0f02
commit fe1a72e41b
12 changed files with 478 additions and 50 deletions
+68 -9
View File
@@ -521,22 +521,19 @@ func (b *backend) ReadResource(ctx context.Context, req *resource.ReadRequest) *
// TODO: validate key ?
if req.ResourceVersion > 0 {
return b.readHistory(ctx, req.Key, req.ResourceVersion)
}
readReq := &sqlResourceReadRequest{
SQLTemplate: sqltemplate.New(b.dialect),
Request: req,
Response: NewReadResponse(),
}
sr := sqlResourceRead
if req.ResourceVersion > 0 {
// read a specific version
sr = sqlResourceHistoryRead
}
var res *resource.BackendReadResponse
err := b.db.WithTx(ctx, ReadCommittedRO, func(ctx context.Context, tx db.Tx) error {
var err error
res, err = dbutil.QueryRow(ctx, tx, sr, readReq)
res, err = dbutil.QueryRow(ctx, tx, sqlResourceRead, readReq)
return err
})
@@ -745,7 +742,38 @@ func (b *backend) listAtRevision(ctx context.Context, req *resource.ListRequest,
return iter.listRV, err
}
// getHistory fetches the resources from the resource table.
// readHistory fetches the resource history from the resource_history table.
func (b *backend) readHistory(ctx context.Context, key *resource.ResourceKey, rv int64) *resource.BackendReadResponse {
_, span := b.tracer.Start(ctx, tracePrefix+".ReadHistory")
defer span.End()
readReq := &sqlResourceHistoryReadRequest{
SQLTemplate: sqltemplate.New(b.dialect),
Request: &historyReadRequest{
Key: key,
ResourceVersion: rv,
},
Response: NewReadResponse(),
}
var res *resource.BackendReadResponse
err := b.db.WithTx(ctx, ReadCommittedRO, func(ctx context.Context, tx db.Tx) error {
var err error
res, err = dbutil.QueryRow(ctx, tx, sqlResourceHistoryRead, readReq)
return err
})
if errors.Is(err, sql.ErrNoRows) {
return &resource.BackendReadResponse{Error: resource.NewNotFoundError(key)}
}
if err != nil {
return &resource.BackendReadResponse{Error: resource.AsErrorResult(err)}
}
return res
}
// getHistory fetches the resource history from the resource_history table.
func (b *backend) getHistory(ctx context.Context, req *resource.ListRequest, cb func(resource.ListIterator) error) (int64, error) {
ctx, span := b.tracer.Start(ctx, tracePrefix+"getHistory")
defer span.End()
@@ -784,6 +812,9 @@ func (b *backend) getHistory(ctx context.Context, req *resource.ListRequest, cb
listReq.MinRV = req.ResourceVersion
}
// Ignore last deleted history record when listing the trash, using exact matching or not older than matching with a specific RV
useLatestDeletionAsMinRV := listReq.MinRV == 0 && !listReq.Trash && req.VersionMatchV2 != resource.ResourceVersionMatchV2_Exact
err := b.db.WithTx(ctx, ReadCommittedRO, func(ctx context.Context, tx db.Tx) error {
var err error
iter.listRV, err = b.fetchLatestRV(ctx, tx, b.dialect, req.Options.Key.Group, req.Options.Key.Resource)
@@ -791,6 +822,14 @@ func (b *backend) getHistory(ctx context.Context, req *resource.ListRequest, cb
return err
}
if useLatestDeletionAsMinRV {
latestDeletedRV, err := b.fetchLatestHistoryRV(ctx, tx, b.dialect, req.Options.Key, resource.WatchEvent_DELETED)
if err != nil {
return err
}
listReq.MinRV = latestDeletedRV + 1
}
rows, err := dbutil.QueryRows(ctx, tx, sqlResourceHistoryGet, listReq)
if rows != nil {
defer func() {
@@ -860,3 +899,23 @@ func (b *backend) fetchLatestRV(ctx context.Context, x db.ContextExecer, d sqlte
}
return res.ResourceVersion, nil
}
// fetchLatestHistoryRV returns the current maximum RV in the resource_history table
func (b *backend) fetchLatestHistoryRV(ctx context.Context, x db.ContextExecer, d sqltemplate.Dialect, key *resource.ResourceKey, eventType resource.WatchEvent_Type) (int64, error) {
ctx, span := b.tracer.Start(ctx, tracePrefix+"fetchLatestHistoryRV")
defer span.End()
res, err := dbutil.QueryRow(ctx, x, sqlResourceHistoryReadLatestRV, sqlResourceHistoryReadLatestRVRequest{
SQLTemplate: sqltemplate.New(d),
Request: &historyReadLatestRVRequest{
Key: key,
EventType: eventType,
},
Response: new(resourceHistoryReadLatestRVResponse),
})
if errors.Is(err, sql.ErrNoRows) {
return 0, nil
} else if err != nil {
return 0, fmt.Errorf("get resource version: %w", err)
}
return res.ResourceVersion, nil
}
+178 -23
View File
@@ -9,8 +9,6 @@ import (
sqlmock "github.com/DATA-DOG/go-sqlmock"
"github.com/mattn/go-sqlite3"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"github.com/grafana/grafana/pkg/apimachinery/utils"
unifiedbackend "github.com/grafana/grafana/pkg/storage/unified/backend"
@@ -18,6 +16,8 @@ import (
"github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl"
"github.com/grafana/grafana/pkg/storage/unified/sql/test"
"github.com/grafana/grafana/pkg/util/testutil"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
var (
@@ -401,6 +401,129 @@ func TestBackend_delete(t *testing.T) {
})
}
type readHistoryRow struct {
namespace string
group string
resource string
name string
folder string
resource_version string
value string
}
func TestBackend_ReadResource(t *testing.T) {
t.Parallel()
t.Run("happy path", func(t *testing.T) {
t.Parallel()
b, ctx := setupBackendTest(t)
expectedReadRow := readHistoryRow{
namespace: "ns",
group: "gr",
resource: "rs",
name: "nm",
folder: "folder",
resource_version: "300",
value: "rv-300",
}
readResource := []string{"namespace", "group", "resource", "name", "folder", "resource_version", "value"}
b.SQLMock.ExpectBegin()
b.SQLMock.ExpectQuery("SELECT .* FROM resource").
WillReturnRows(sqlmock.NewRows(readResource).
AddRow(
expectedReadRow.namespace,
expectedReadRow.group,
expectedReadRow.resource,
expectedReadRow.name,
expectedReadRow.folder,
expectedReadRow.resource_version,
expectedReadRow.value,
))
b.SQLMock.ExpectCommit()
req := &resource.ReadRequest{
Key: resKey,
}
rps := b.ReadResource(ctx, req)
require.NotNil(t, rps)
require.Equal(t, int64(300), rps.ResourceVersion)
require.Equal(t, "rv-300", string(rps.Value))
require.Equal(t, "folder", rps.Folder)
})
t.Run("no resource found", func(t *testing.T) {
t.Parallel()
b, ctx := setupBackendTest(t)
b.SQLMock.ExpectBegin()
b.SQLMock.ExpectQuery("SELECT .* FROM resource").
WillReturnRows(sqlmock.NewRows([]string{}))
b.SQLMock.ExpectCommit()
req := &resource.ReadRequest{
Key: resKey,
}
res := b.ReadResource(ctx, req)
require.NotNil(t, res.Error)
require.Equal(t, res.Error.Code, int32(404))
})
t.Run("with resource version", func(t *testing.T) {
t.Parallel()
b, ctx := setupBackendTest(t)
expectedReadRow := readHistoryRow{
namespace: "ns",
group: "gr",
resource: "rs",
name: "nm",
folder: "folder",
resource_version: "300",
value: "rv-300",
}
readHistoryColumns := []string{"namespace", "group", "resource", "name", "folder", "resource_version", "value"}
b.SQLMock.ExpectBegin()
b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").
WillReturnRows(sqlmock.NewRows(readHistoryColumns).
AddRow(
expectedReadRow.namespace,
expectedReadRow.group,
expectedReadRow.resource,
expectedReadRow.name,
expectedReadRow.folder,
expectedReadRow.resource_version,
expectedReadRow.value,
))
b.SQLMock.ExpectCommit()
req := &resource.ReadRequest{
Key: resKey,
ResourceVersion: 300,
}
rps := b.ReadResource(ctx, req)
require.NotNil(t, rps)
require.Equal(t, int64(300), rps.ResourceVersion)
require.Equal(t, "rv-300", string(rps.Value))
require.Equal(t, "folder", rps.Folder)
})
t.Run("error reading resource", func(t *testing.T) {
t.Parallel()
b, ctx := setupBackendTest(t)
b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").
WillReturnError(errTest)
req := &resource.ReadRequest{
Key: resKey,
}
rps := b.ReadResource(ctx, req)
require.NotNil(t, rps.Error)
})
}
func TestBackend_getHistory(t *testing.T) {
t.Parallel()
@@ -415,16 +538,19 @@ func TestBackend_getHistory(t *testing.T) {
cols := []string{"resource_version", "namespace", "name", "folder", "value"}
tests := []struct {
name string
versionMatch resource.ResourceVersionMatchV2
resourceVersion int64
expectedVersions []int64
expectedListRv int64
expectedRowsCount int
expectedErr string
name string
source resource.ListRequest_Source
versionMatch resource.ResourceVersionMatchV2
resourceVersion int64
expectedVersions []int64
expectedListRv int64
expectedRowsCount int
expectedErr string
expectedLatestDeletionAsMinRV bool
}{
{
name: "with ResourceVersionMatch_NotOlderThan",
source: resource.ListRequest_HISTORY,
versionMatch: resource.ResourceVersionMatchV2_NotOlderThan,
resourceVersion: rv2,
expectedVersions: []int64{rv2, rv3}, // Should be in ASC order due to NotOlderThan
@@ -432,15 +558,18 @@ func TestBackend_getHistory(t *testing.T) {
expectedRowsCount: 2,
},
{
name: "with ResourceVersionMatch_NotOlderThan and ResourceVersion=0",
versionMatch: resource.ResourceVersionMatchV2_NotOlderThan,
resourceVersion: 0,
expectedVersions: []int64{rv1, rv2, rv3}, // Should be in ASC order due to NotOlderThan
expectedListRv: rv3,
expectedRowsCount: 3,
name: "with ResourceVersionMatch_NotOlderThan and ResourceVersion=0",
source: resource.ListRequest_HISTORY,
versionMatch: resource.ResourceVersionMatchV2_NotOlderThan,
resourceVersion: 0,
expectedVersions: []int64{rv1, rv2, rv3}, // Should be in ASC order due to NotOlderThan
expectedListRv: rv3,
expectedRowsCount: 3,
expectedLatestDeletionAsMinRV: true,
},
{
name: "with ResourceVersionMatch_Exact",
source: resource.ListRequest_HISTORY,
versionMatch: resource.ResourceVersionMatchV2_Exact,
resourceVersion: rv2,
expectedVersions: []int64{rv2},
@@ -448,17 +577,27 @@ func TestBackend_getHistory(t *testing.T) {
expectedRowsCount: 1,
},
{
name: "with ResourceVersionMatch_Unset (default)",
expectedVersions: []int64{rv3, rv2, rv1}, // Should be in DESC order by default
expectedListRv: rv3,
expectedRowsCount: 3,
name: "with ResourceVersionMatch_Unset (default)",
source: resource.ListRequest_HISTORY,
expectedVersions: []int64{rv3, rv2, rv1}, // Should be in DESC order by default
expectedListRv: rv3,
expectedRowsCount: 3,
expectedLatestDeletionAsMinRV: true,
},
{
name: "error with ResourceVersionMatch_Exact and ResourceVersion <= 0",
source: resource.ListRequest_HISTORY,
versionMatch: resource.ResourceVersionMatchV2_Exact,
resourceVersion: 0,
expectedErr: "expecting an explicit resource version query when using Exact matching",
},
{
name: "with ListRequest_TRASH",
source: resource.ListRequest_TRASH,
expectedVersions: []int64{rv3, rv2, rv1}, // Should be in DESC order by default
expectedListRv: rv3,
expectedRowsCount: 3,
},
}
for _, tc := range tests {
@@ -472,7 +611,7 @@ func TestBackend_getHistory(t *testing.T) {
Options: &resource.ListOptions{Key: key},
ResourceVersion: tc.resourceVersion,
VersionMatchV2: tc.versionMatch,
Source: resource.ListRequest_HISTORY,
Source: tc.source,
}
// Set up mock expectations only if we don't expect an error
@@ -508,6 +647,13 @@ func TestBackend_getHistory(t *testing.T) {
AddRow(rv3, 0)
b.SQLMock.ExpectQuery("SELECT .* FROM resource_version").WillReturnRows(latestRVRows)
if tc.expectedLatestDeletionAsMinRV {
latestHistoryRVRows := sqlmock.NewRows([]string{"resource_version"}).
AddRow(rv1)
b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").
WillReturnRows(latestHistoryRVRows)
}
// Expect history query
historyRows := sqlmock.NewRows(cols)
for _, rv := range tc.expectedVersions {
@@ -606,6 +752,7 @@ func TestBackend_getHistoryPagination(t *testing.T) {
req.NextPageToken = page.token.String()
}
expectedLatestDeletionAsMinRV := false
items := make([]int64, 0)
callback := func(iter resource.ListIterator) error {
for iter.Next() {
@@ -615,7 +762,7 @@ func TestBackend_getHistoryPagination(t *testing.T) {
}
b.SQLMock.ExpectBegin()
historyRows := setupHistoryTest(b, page.versions, rv60)
historyRows := setupHistoryTest(b, page.versions, rv60, expectedLatestDeletionAsMinRV)
b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").WillReturnRows(historyRows)
b.SQLMock.ExpectCommit()
@@ -643,6 +790,7 @@ func TestBackend_getHistoryPagination(t *testing.T) {
Limit: 4,
}
expectedLatestDeletionAsMinRV := true
// First batch of items we expect, in ASC order (because of NotOlderThan flag)
// Even with ResourceVersion=0, the order is ASC because we use SortAscending=true
expectedVersions := []int64{rv51, rv52, rv53, rv54}
@@ -656,7 +804,7 @@ func TestBackend_getHistoryPagination(t *testing.T) {
}
b.SQLMock.ExpectBegin()
historyRows := setupHistoryTest(b, expectedVersions, rv60)
historyRows := setupHistoryTest(b, expectedVersions, rv60, expectedLatestDeletionAsMinRV)
b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").WillReturnRows(historyRows)
b.SQLMock.ExpectCommit()
@@ -668,12 +816,19 @@ func TestBackend_getHistoryPagination(t *testing.T) {
}
// setupHistoryTest creates the necessary mock expectations for a history test
func setupHistoryTest(b testBackend, resourceVersions []int64, latestRV int64) *sqlmock.Rows {
func setupHistoryTest(b testBackend, resourceVersions []int64, latestRV int64, expectedLatestDeletionAsMinRV4 bool) *sqlmock.Rows {
// Expect fetch latest RV call - set to the highest resource version
latestRVRows := sqlmock.NewRows([]string{"resource_version", "unix_timestamp"}).
AddRow(latestRV, 0)
b.SQLMock.ExpectQuery("SELECT .* FROM resource_version").WillReturnRows(latestRVRows)
if expectedLatestDeletionAsMinRV4 {
latestHistoryRVRows := sqlmock.NewRows([]string{"resource_version"}).
AddRow(latestRV)
b.SQLMock.ExpectQuery("SELECT .* FROM resource_history").
WillReturnRows(latestHistoryRVRows)
}
// Create the mock rows for the history items
cols := []string{"resource_version", "namespace", "name", "folder", "value"}
historyRows := sqlmock.NewRows(cols)
@@ -0,0 +1,14 @@
SELECT
COALESCE(MAX({{ .Ident "resource_version" }}), 0) AS {{ .Ident "resource_version" | .Into .Response.ResourceVersion }}
FROM {{ .Ident "resource_history" }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Request.Key.Namespace }}
AND {{ .Ident "group" }} = {{ .Arg .Request.Key.Group }}
AND {{ .Ident "resource" }} = {{ .Arg .Request.Key.Resource }}
AND {{ .Ident "name" }} = {{ .Arg .Request.Key.Name }}
{{ if gt .Request.EventType 0 }}
AND {{ .Ident "action" }} = {{ .Arg .Request.EventType }}
{{ end }}
LIMIT 1
;
+63 -16
View File
@@ -29,22 +29,23 @@ func mustTemplate(filename string) *template.Template {
// Templates.
var (
sqlResourceDelete = mustTemplate("resource_delete.sql")
sqlResourceInsert = mustTemplate("resource_insert.sql")
sqlResourceUpdate = mustTemplate("resource_update.sql")
sqlResourceRead = mustTemplate("resource_read.sql")
sqlResourceStats = mustTemplate("resource_stats.sql")
sqlResourceList = mustTemplate("resource_list.sql")
sqlResourceHistoryList = mustTemplate("resource_history_list.sql")
sqlResourceUpdateRV = mustTemplate("resource_update_rv.sql")
sqlResourceHistoryRead = mustTemplate("resource_history_read.sql")
sqlResourceHistoryUpdateRV = mustTemplate("resource_history_update_rv.sql")
sqlResourceHistoryInsert = mustTemplate("resource_history_insert.sql")
sqlResourceHistoryPoll = mustTemplate("resource_history_poll.sql")
sqlResourceHistoryGet = mustTemplate("resource_history_get.sql")
sqlResourceHistoryDelete = mustTemplate("resource_history_delete.sql")
sqlResourceHistoryPrune = mustTemplate("resource_history_prune.sql")
sqlResourceInsertFromHistory = mustTemplate("resource_insert_from_history.sql")
sqlResourceDelete = mustTemplate("resource_delete.sql")
sqlResourceInsert = mustTemplate("resource_insert.sql")
sqlResourceUpdate = mustTemplate("resource_update.sql")
sqlResourceRead = mustTemplate("resource_read.sql")
sqlResourceStats = mustTemplate("resource_stats.sql")
sqlResourceList = mustTemplate("resource_list.sql")
sqlResourceHistoryList = mustTemplate("resource_history_list.sql")
sqlResourceUpdateRV = mustTemplate("resource_update_rv.sql")
sqlResourceHistoryRead = mustTemplate("resource_history_read.sql")
sqlResourceHistoryReadLatestRV = mustTemplate("resource_history_read_latest_rv.sql")
sqlResourceHistoryUpdateRV = mustTemplate("resource_history_update_rv.sql")
sqlResourceHistoryInsert = mustTemplate("resource_history_insert.sql")
sqlResourceHistoryPoll = mustTemplate("resource_history_poll.sql")
sqlResourceHistoryGet = mustTemplate("resource_history_get.sql")
sqlResourceHistoryDelete = mustTemplate("resource_history_delete.sql")
sqlResourceHistoryPrune = mustTemplate("resource_history_prune.sql")
sqlResourceInsertFromHistory = mustTemplate("resource_insert_from_history.sql")
// sqlResourceLabelsInsert = mustTemplate("resource_labels_insert.sql")
sqlResourceVersionGet = mustTemplate("resource_version_get.sql")
@@ -190,6 +191,52 @@ func (r sqlResourceListRequest) Validate() error {
return nil // TODO
}
type historyReadRequest struct {
Key *resource.ResourceKey
ResourceVersion int64
}
type sqlResourceHistoryReadRequest struct {
sqltemplate.SQLTemplate
Request *historyReadRequest
Response *resource.BackendReadResponse
}
func (r sqlResourceHistoryReadRequest) Validate() error {
return nil // TODO
}
func (r sqlResourceHistoryReadRequest) Results() (*resource.BackendReadResponse, error) {
return r.Response, nil
}
type historyReadLatestRVRequest struct {
Key *resource.ResourceKey
EventType resource.WatchEvent_Type
}
type sqlResourceHistoryReadLatestRVRequest struct {
sqltemplate.SQLTemplate
Request *historyReadLatestRVRequest
Response *resourceHistoryReadLatestRVResponse
}
func (r sqlResourceHistoryReadLatestRVRequest) Validate() error {
return nil // TODO
}
func (r sqlResourceHistoryReadLatestRVRequest) Results() (*resourceHistoryReadLatestRVResponse, error) {
return r.Response, nil
}
type resourceHistoryReadLatestRVResponse struct {
ResourceVersion int64
}
func (r *resourceHistoryReadLatestRVResponse) Results() (*resourceHistoryReadLatestRVResponse, error) {
return r, nil
}
type historyListRequest struct {
ResourceVersion, Limit, Offset int64
Folder string
+36 -2
View File
@@ -147,9 +147,9 @@ func TestUnifiedStorageQueries(t *testing.T) {
sqlResourceHistoryRead: {
{
Name: "single path",
Data: &sqlResourceReadRequest{
Data: &sqlResourceHistoryReadRequest{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Request: &resource.ReadRequest{
Request: &historyReadRequest{
ResourceVersion: 123,
Key: &resource.ResourceKey{
Namespace: "ns",
@@ -163,6 +163,40 @@ func TestUnifiedStorageQueries(t *testing.T) {
},
},
sqlResourceHistoryReadLatestRV: {
{
Name: "single path",
Data: &sqlResourceHistoryReadLatestRVRequest{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Request: &historyReadLatestRVRequest{
Key: &resource.ResourceKey{
Namespace: "ns",
Group: "gp",
Resource: "rs",
Name: "nm",
},
},
Response: new(resourceHistoryReadLatestRVResponse),
},
},
{
Name: "with WatchEvent_DELETED",
Data: &sqlResourceHistoryReadLatestRVRequest{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Request: &historyReadLatestRVRequest{
Key: &resource.ResourceKey{
Namespace: "ns",
Group: "gp",
Resource: "rs",
Name: "nm",
},
EventType: resource.WatchEvent_DELETED,
},
Response: new(resourceHistoryReadLatestRVResponse),
},
},
},
sqlResourceHistoryUpdateRV: {
{
Name: "single path",
@@ -0,0 +1,9 @@
SELECT
COALESCE(MAX(`resource_version`), 0) AS `resource_version`
FROM `resource_history`
WHERE `namespace` = 'ns'
AND `group` = 'gp'
AND `resource` = 'rs'
AND `name` = 'nm'
LIMIT 1
;
@@ -0,0 +1,10 @@
SELECT
COALESCE(MAX(`resource_version`), 0) AS `resource_version`
FROM `resource_history`
WHERE `namespace` = 'ns'
AND `group` = 'gp'
AND `resource` = 'rs'
AND `name` = 'nm'
AND `action` = 'DELETED'
LIMIT 1
;
@@ -0,0 +1,9 @@
SELECT
COALESCE(MAX("resource_version"), 0) AS "resource_version"
FROM "resource_history"
WHERE "namespace" = 'ns'
AND "group" = 'gp'
AND "resource" = 'rs'
AND "name" = 'nm'
LIMIT 1
;
@@ -0,0 +1,10 @@
SELECT
COALESCE(MAX("resource_version"), 0) AS "resource_version"
FROM "resource_history"
WHERE "namespace" = 'ns'
AND "group" = 'gp'
AND "resource" = 'rs'
AND "name" = 'nm'
AND "action" = 'DELETED'
LIMIT 1
;
@@ -0,0 +1,9 @@
SELECT
COALESCE(MAX("resource_version"), 0) AS "resource_version"
FROM "resource_history"
WHERE "namespace" = 'ns'
AND "group" = 'gp'
AND "resource" = 'rs'
AND "name" = 'nm'
LIMIT 1
;
@@ -0,0 +1,10 @@
SELECT
COALESCE(MAX("resource_version"), 0) AS "resource_version"
FROM "resource_history"
WHERE "namespace" = 'ns'
AND "group" = 'gp'
AND "resource" = 'rs'
AND "name" = 'nm'
AND "action" = 'DELETED'
LIMIT 1
;
@@ -769,6 +769,68 @@ func runTestIntegrationBackendListHistory(t *testing.T, backend resource.Storage
require.Equal(t, resourceVersions[i], allItems[i].ResourceVersion)
}
})
t.Run("fetch history with deleted item", func(t *testing.T) {
// Create a resource and delete it
rv, err := writeEvent(ctx, backend, "deleted-item", resource.WatchEvent_ADDED, WithNamespace(ns))
require.NoError(t, err)
rvDeleted, err := writeEvent(ctx, backend, "deleted-item", resource.WatchEvent_DELETED, WithNamespace(ns))
require.NoError(t, err)
require.Greater(t, rvDeleted, rv)
// Fetch history for the deleted item
res, err := server.List(ctx, &resource.ListRequest{
Source: resource.ListRequest_HISTORY,
Options: &resource.ListOptions{
Key: &resource.ResourceKey{
Namespace: ns,
Group: "group",
Resource: "resource",
Name: "deleted-item",
},
},
})
require.NoError(t, err)
require.Nil(t, res.Error)
require.Len(t, res.Items, 0)
})
t.Run("fetch history with recreated item", func(t *testing.T) {
// Create a resource and delete it
rv, err := writeEvent(ctx, backend, "deleted-item", resource.WatchEvent_ADDED, WithNamespace(ns))
require.NoError(t, err)
rvDeleted, err := writeEvent(ctx, backend, "deleted-item", resource.WatchEvent_DELETED, WithNamespace(ns))
require.NoError(t, err)
require.Greater(t, rvDeleted, rv)
// Create a few more versions after deletion
rv1, err := writeEvent(ctx, backend, "deleted-item", resource.WatchEvent_ADDED, WithNamespace(ns))
require.NoError(t, err)
require.Greater(t, rv1, rvDeleted)
rv2, err := writeEvent(ctx, backend, "deleted-item", resource.WatchEvent_MODIFIED, WithNamespace(ns))
require.NoError(t, err)
require.Greater(t, rv2, rv1)
// Fetch history for the deleted item
res, err := server.List(ctx, &resource.ListRequest{
Source: resource.ListRequest_HISTORY,
Options: &resource.ListOptions{
Key: &resource.ResourceKey{
Namespace: ns,
Group: "group",
Resource: "resource",
Name: "deleted-item",
},
},
})
require.NoError(t, err)
require.Nil(t, res.Error)
require.Len(t, res.Items, 2)
require.Equal(t, "deleted-item MODIFIED", string(res.Items[0].Value))
require.Equal(t, rv2, res.Items[0].ResourceVersion)
require.Equal(t, "deleted-item ADDED", string(res.Items[1].Value))
require.Equal(t, rv1, res.Items[1].ResourceVersion)
})
}
func runTestIntegrationBlobSupport(t *testing.T, backend resource.StorageBackend, nsPrefix string) {