|
|
|
@ -8,6 +8,7 @@ import (
|
|
|
|
|
"strings" |
|
|
|
|
"sync/atomic" |
|
|
|
|
"testing" |
|
|
|
|
"time" |
|
|
|
|
|
|
|
|
|
"github.com/oklog/ulid/v2" |
|
|
|
|
"github.com/stretchr/testify/mock" |
|
|
|
@ -19,8 +20,10 @@ import (
|
|
|
|
|
"github.com/hashicorp/consul/acl/resolver" |
|
|
|
|
"github.com/hashicorp/consul/internal/resource" |
|
|
|
|
"github.com/hashicorp/consul/internal/resource/demo" |
|
|
|
|
rtest "github.com/hashicorp/consul/internal/resource/resourcetest" |
|
|
|
|
"github.com/hashicorp/consul/internal/storage" |
|
|
|
|
"github.com/hashicorp/consul/proto-public/pbresource" |
|
|
|
|
pbdemo "github.com/hashicorp/consul/proto/private/pbdemo/v1" |
|
|
|
|
pbdemov1 "github.com/hashicorp/consul/proto/private/pbdemo/v1" |
|
|
|
|
pbdemov2 "github.com/hashicorp/consul/proto/private/pbdemo/v2" |
|
|
|
|
"github.com/hashicorp/consul/proto/private/prototest" |
|
|
|
@ -457,7 +460,24 @@ func TestWrite_Create_Tenancy_NotFound(t *testing.T) {
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestWrite_Tenancy_MarkedForDeletion(t *testing.T) { |
|
|
|
|
func TestWrite_Create_With_DeletionTimestamp_Fails(t *testing.T) { |
|
|
|
|
server := testServer(t) |
|
|
|
|
client := testClient(t, server) |
|
|
|
|
demo.RegisterTypes(server.Registry) |
|
|
|
|
|
|
|
|
|
res := rtest.Resource(demo.TypeV1Artist, "blur"). |
|
|
|
|
WithTenancy(resource.DefaultNamespacedTenancy()). |
|
|
|
|
WithData(t, &pbdemov1.Artist{Name: "Blur"}). |
|
|
|
|
WithMeta(resource.DeletionTimestampKey, time.Now().Format(time.RFC3339)). |
|
|
|
|
Build() |
|
|
|
|
|
|
|
|
|
_, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res}) |
|
|
|
|
require.Error(t, err) |
|
|
|
|
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) |
|
|
|
|
require.Contains(t, err.Error(), resource.DeletionTimestampKey) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestWrite_Create_With_TenancyMarkedForDeletion_Fails(t *testing.T) { |
|
|
|
|
// Verify resource write fails when its partition or namespace is marked for deletion.
|
|
|
|
|
testCases := map[string]struct { |
|
|
|
|
modFn func(artist, recordLabel *pbresource.Resource, mockTenancyBridge *MockTenancyBridge) *pbresource.Resource |
|
|
|
@ -468,7 +488,7 @@ func TestWrite_Tenancy_MarkedForDeletion(t *testing.T) {
|
|
|
|
|
mockTenancyBridge.On("IsPartitionMarkedForDeletion", "ap1").Return(true, nil) |
|
|
|
|
return artist |
|
|
|
|
}, |
|
|
|
|
errContains: "partition marked for deletion", |
|
|
|
|
errContains: "tenancy marked for deletion", |
|
|
|
|
}, |
|
|
|
|
"namespaced resources namespace marked for deletion": { |
|
|
|
|
modFn: func(artist, _ *pbresource.Resource, mockTenancyBridge *MockTenancyBridge) *pbresource.Resource { |
|
|
|
@ -476,14 +496,14 @@ func TestWrite_Tenancy_MarkedForDeletion(t *testing.T) {
|
|
|
|
|
mockTenancyBridge.On("IsNamespaceMarkedForDeletion", "ap1", "ns1").Return(true, nil) |
|
|
|
|
return artist |
|
|
|
|
}, |
|
|
|
|
errContains: "namespace marked for deletion", |
|
|
|
|
errContains: "tenancy marked for deletion", |
|
|
|
|
}, |
|
|
|
|
"partitioned resources partition marked for deletion": { |
|
|
|
|
modFn: func(_, recordLabel *pbresource.Resource, mockTenancyBridge *MockTenancyBridge) *pbresource.Resource { |
|
|
|
|
mockTenancyBridge.On("IsPartitionMarkedForDeletion", "ap1").Return(true, nil) |
|
|
|
|
return recordLabel |
|
|
|
|
}, |
|
|
|
|
errContains: "partition marked for deletion", |
|
|
|
|
errContains: "tenancy marked for deletion", |
|
|
|
|
}, |
|
|
|
|
} |
|
|
|
|
for desc, tc := range testCases { |
|
|
|
@ -903,3 +923,168 @@ func (b *blockOnceBackend) Read(ctx context.Context, consistency storage.ReadCon
|
|
|
|
|
|
|
|
|
|
return res, err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestEnsureFinalizerRemoved(t *testing.T) { |
|
|
|
|
type testCase struct { |
|
|
|
|
mod func(input, existing *pbresource.Resource) |
|
|
|
|
errContains string |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
testCases := map[string]testCase{ |
|
|
|
|
"one finalizer removed from input": { |
|
|
|
|
mod: func(input, existing *pbresource.Resource) { |
|
|
|
|
resource.AddFinalizer(existing, "f1") |
|
|
|
|
resource.AddFinalizer(existing, "f2") |
|
|
|
|
resource.AddFinalizer(input, "f1") |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
"all finalizers removed from input": { |
|
|
|
|
mod: func(input, existing *pbresource.Resource) { |
|
|
|
|
resource.AddFinalizer(existing, "f1") |
|
|
|
|
resource.AddFinalizer(existing, "f2") |
|
|
|
|
resource.AddFinalizer(input, "f1") |
|
|
|
|
resource.RemoveFinalizer(input, "f1") |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
"all finalizers removed from input and no finalizer key": { |
|
|
|
|
mod: func(input, existing *pbresource.Resource) { |
|
|
|
|
resource.AddFinalizer(existing, "f1") |
|
|
|
|
resource.AddFinalizer(existing, "f2") |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
"no finalizers removed from input": { |
|
|
|
|
mod: func(input, existing *pbresource.Resource) { |
|
|
|
|
resource.AddFinalizer(existing, "f1") |
|
|
|
|
resource.AddFinalizer(input, "f1") |
|
|
|
|
}, |
|
|
|
|
errContains: "expected at least one finalizer to be removed", |
|
|
|
|
}, |
|
|
|
|
"input finalizers not proper subset of existing": { |
|
|
|
|
mod: func(input, existing *pbresource.Resource) { |
|
|
|
|
resource.AddFinalizer(existing, "f1") |
|
|
|
|
resource.AddFinalizer(existing, "f2") |
|
|
|
|
resource.AddFinalizer(input, "f3") |
|
|
|
|
}, |
|
|
|
|
errContains: "expected at least one finalizer to be removed", |
|
|
|
|
}, |
|
|
|
|
"existing has no finalizers for input to remove": { |
|
|
|
|
mod: func(input, existing *pbresource.Resource) { |
|
|
|
|
resource.AddFinalizer(input, "f3") |
|
|
|
|
}, |
|
|
|
|
errContains: "expected at least one finalizer to be removed", |
|
|
|
|
}, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
for desc, tc := range testCases { |
|
|
|
|
t.Run(desc, func(t *testing.T) { |
|
|
|
|
input := rtest.Resource(demo.TypeV1Artist, "artist1"). |
|
|
|
|
WithTenancy(resource.DefaultNamespacedTenancy()). |
|
|
|
|
WithData(t, &pbdemov1.Artist{Name: "artist1"}). |
|
|
|
|
WithMeta(resource.DeletionTimestampKey, "someTimestamp"). |
|
|
|
|
Build() |
|
|
|
|
|
|
|
|
|
existing := rtest.Resource(demo.TypeV1Artist, "artist1"). |
|
|
|
|
WithTenancy(resource.DefaultNamespacedTenancy()). |
|
|
|
|
WithData(t, &pbdemov1.Artist{Name: "artist1"}). |
|
|
|
|
WithMeta(resource.DeletionTimestampKey, "someTimestamp"). |
|
|
|
|
Build() |
|
|
|
|
|
|
|
|
|
tc.mod(input, existing) |
|
|
|
|
|
|
|
|
|
err := ensureFinalizerRemoved(input, existing) |
|
|
|
|
if tc.errContains != "" { |
|
|
|
|
require.Error(t, err) |
|
|
|
|
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) |
|
|
|
|
require.ErrorContains(t, err, tc.errContains) |
|
|
|
|
} else { |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func TestWrite_ResourceFrozenAfterMarkedForDeletion(t *testing.T) { |
|
|
|
|
type testCase struct { |
|
|
|
|
modFn func(res *pbresource.Resource) |
|
|
|
|
errContains string |
|
|
|
|
} |
|
|
|
|
testCases := map[string]testCase{ |
|
|
|
|
"no-op write rejected": { |
|
|
|
|
modFn: func(res *pbresource.Resource) {}, |
|
|
|
|
errContains: "no-op write of resource marked for deletion not allowed", |
|
|
|
|
}, |
|
|
|
|
"remove one finalizer": { |
|
|
|
|
modFn: func(res *pbresource.Resource) { |
|
|
|
|
resource.RemoveFinalizer(res, "finalizer1") |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
"remove all finalizers": { |
|
|
|
|
modFn: func(res *pbresource.Resource) { |
|
|
|
|
resource.RemoveFinalizer(res, "finalizer1") |
|
|
|
|
resource.RemoveFinalizer(res, "finalizer2") |
|
|
|
|
}, |
|
|
|
|
}, |
|
|
|
|
"adding finalizer fails": { |
|
|
|
|
modFn: func(res *pbresource.Resource) { |
|
|
|
|
resource.AddFinalizer(res, "finalizer3") |
|
|
|
|
}, |
|
|
|
|
errContains: "expected at least one finalizer to be removed", |
|
|
|
|
}, |
|
|
|
|
"remove deletionTimestamp fails": { |
|
|
|
|
modFn: func(res *pbresource.Resource) { |
|
|
|
|
delete(res.Metadata, resource.DeletionTimestampKey) |
|
|
|
|
}, |
|
|
|
|
errContains: "cannot remove deletionTimestamp", |
|
|
|
|
}, |
|
|
|
|
"modify deletionTimestamp fails": { |
|
|
|
|
modFn: func(res *pbresource.Resource) { |
|
|
|
|
res.Metadata[resource.DeletionTimestampKey] = "bad" |
|
|
|
|
}, |
|
|
|
|
errContains: "cannot modify deletionTimestamp", |
|
|
|
|
}, |
|
|
|
|
"modify data fails": { |
|
|
|
|
modFn: func(res *pbresource.Resource) { |
|
|
|
|
var err error |
|
|
|
|
res.Data, err = anypb.New(&pbdemo.Artist{Name: "New Order"}) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
}, |
|
|
|
|
errContains: "cannot modify data", |
|
|
|
|
}, |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
for desc, tc := range testCases { |
|
|
|
|
t.Run(desc, func(t *testing.T) { |
|
|
|
|
server, client, ctx := testDeps(t) |
|
|
|
|
demo.RegisterTypes(server.Registry) |
|
|
|
|
|
|
|
|
|
// Create a resource with finalizers
|
|
|
|
|
res := rtest.Resource(demo.TypeV1Artist, "joydivision"). |
|
|
|
|
WithTenancy(resource.DefaultNamespacedTenancy()). |
|
|
|
|
WithData(t, &pbdemo.Artist{Name: "Joy Division"}). |
|
|
|
|
WithMeta(resource.FinalizerKey, "finalizer1 finalizer2"). |
|
|
|
|
Write(t, client) |
|
|
|
|
|
|
|
|
|
// Mark for deletion - resource should now be frozen
|
|
|
|
|
_, err := client.Delete(ctx, &pbresource.DeleteRequest{Id: res.Id}) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
|
|
|
|
|
// Verify marked for deletion
|
|
|
|
|
rsp, err := client.Read(ctx, &pbresource.ReadRequest{Id: res.Id}) |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
require.True(t, resource.IsMarkedForDeletion(rsp.Resource)) |
|
|
|
|
|
|
|
|
|
// Apply test case mods
|
|
|
|
|
tc.modFn(rsp.Resource) |
|
|
|
|
|
|
|
|
|
// Verify write results
|
|
|
|
|
_, err = client.Write(ctx, &pbresource.WriteRequest{Resource: rsp.Resource}) |
|
|
|
|
if tc.errContains == "" { |
|
|
|
|
require.NoError(t, err) |
|
|
|
|
} else { |
|
|
|
|
require.Error(t, err) |
|
|
|
|
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) |
|
|
|
|
require.ErrorContains(t, err, tc.errContains) |
|
|
|
|
} |
|
|
|
|
}) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|