mirror of
https://github.com/status-im/consul.git
synced 2025-01-11 14:24:39 +00:00
34a32d4ce5
The peer name will eventually show up elsewhere in the resource. For now though this rips it out of where we don’t want it to be.
341 lines
10 KiB
Go
341 lines
10 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package controller
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
mockpbresource "github.com/hashicorp/consul/grpcmocks/proto-public/pbresource"
|
|
"github.com/hashicorp/consul/internal/resource"
|
|
"github.com/hashicorp/consul/internal/resource/resourcetest"
|
|
"github.com/hashicorp/consul/internal/storage"
|
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
|
"github.com/hashicorp/consul/proto/private/prototest"
|
|
"github.com/hashicorp/consul/sdk/testutil"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc"
|
|
)
|
|
|
|
var (
|
|
fakeType = &pbresource.Type{
|
|
Group: "testing",
|
|
GroupVersion: "v1",
|
|
Kind: "Fake",
|
|
}
|
|
|
|
fakeV2Type = &pbresource.Type{
|
|
Group: "testing",
|
|
GroupVersion: "v2",
|
|
Kind: "Fake",
|
|
}
|
|
)
|
|
|
|
type memCheckResult struct {
|
|
clientGet *pbresource.Resource
|
|
clientGetError error
|
|
cacheGet *pbresource.Resource
|
|
cacheGetError error
|
|
}
|
|
|
|
type memCheckReconciler struct {
|
|
mu sync.Mutex
|
|
closed bool
|
|
reconcileCh chan memCheckResult
|
|
mapCh chan memCheckResult
|
|
}
|
|
|
|
func newMemCheckReconciler(t testutil.TestingTB) *memCheckReconciler {
|
|
t.Helper()
|
|
|
|
r := &memCheckReconciler{
|
|
reconcileCh: make(chan memCheckResult, 10),
|
|
mapCh: make(chan memCheckResult, 10),
|
|
}
|
|
|
|
t.Cleanup(r.Shutdown)
|
|
return r
|
|
}
|
|
|
|
func (r *memCheckReconciler) Shutdown() {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
r.closed = true
|
|
close(r.reconcileCh)
|
|
close(r.mapCh)
|
|
}
|
|
|
|
func (r *memCheckReconciler) requireNotClosed(t testutil.TestingTB) {
|
|
t.Helper()
|
|
if r.closed {
|
|
require.FailNow(t, "the memCheckReconciler has been closed")
|
|
}
|
|
}
|
|
|
|
func (r *memCheckReconciler) checkReconcileResult(t testutil.TestingTB, ctx context.Context, res *pbresource.Resource) {
|
|
t.Helper()
|
|
r.requireEqualNotSameMemCheckResult(t, ctx, r.reconcileCh, res)
|
|
}
|
|
|
|
func (r *memCheckReconciler) checkMapResult(t testutil.TestingTB, ctx context.Context, res *pbresource.Resource) {
|
|
t.Helper()
|
|
r.requireEqualNotSameMemCheckResult(t, ctx, r.mapCh, res)
|
|
}
|
|
|
|
func (r *memCheckReconciler) requireEqualNotSameMemCheckResult(t testutil.TestingTB, ctx context.Context, ch <-chan memCheckResult, res *pbresource.Resource) {
|
|
t.Helper()
|
|
|
|
select {
|
|
case result := <-ch:
|
|
require.NoError(t, result.clientGetError)
|
|
require.NoError(t, result.cacheGetError)
|
|
// Equal but NotSame means the values are all the same but
|
|
// the pointers are different. Note that this probably doesn't
|
|
// check that the values within the resource haven't been shallow
|
|
// copied but that probably should be checked elsewhere
|
|
prototest.AssertDeepEqual(t, res, result.clientGet)
|
|
require.NotSame(t, res, result.clientGet)
|
|
prototest.AssertDeepEqual(t, res, result.cacheGet)
|
|
require.NotSame(t, res, result.cacheGet)
|
|
case <-ctx.Done():
|
|
require.Fail(t, "didn't receive mem check result before context cancellation", ctx.Err())
|
|
}
|
|
}
|
|
|
|
func (r *memCheckReconciler) Reconcile(ctx context.Context, rt Runtime, req Request) error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if !r.closed {
|
|
r.getAndSend(ctx, rt, req.ID, r.reconcileCh)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *memCheckReconciler) MapToNothing(
|
|
ctx context.Context,
|
|
rt Runtime,
|
|
res *pbresource.Resource,
|
|
) ([]Request, error) {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if !r.closed {
|
|
r.getAndSend(ctx, rt, res.Id, r.mapCh)
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (*memCheckReconciler) getAndSend(ctx context.Context, rt Runtime, id *pbresource.ID, ch chan<- memCheckResult) {
|
|
var res memCheckResult
|
|
response, err := rt.Client.Read(ctx, &pbresource.ReadRequest{
|
|
Id: id,
|
|
})
|
|
res.clientGetError = err
|
|
if response != nil {
|
|
res.clientGet = response.Resource
|
|
}
|
|
|
|
res.cacheGet, res.cacheGetError = rt.Cache.Get(id.Type, "id", id)
|
|
|
|
ch <- res
|
|
}
|
|
|
|
func watchListEvents(t testutil.TestingTB, events ...*pbresource.WatchEvent) pbresource.ResourceService_WatchListClient {
|
|
t.Helper()
|
|
ctx := testutil.TestContext(t)
|
|
|
|
watchListClient := mockpbresource.NewResourceService_WatchListClient(t)
|
|
|
|
// Return the events in the specified order as soon as they are requested
|
|
for _, event := range events {
|
|
watchListClient.EXPECT().
|
|
Recv().
|
|
RunAndReturn(func() (*pbresource.WatchEvent, error) {
|
|
return event, nil
|
|
}).
|
|
Once()
|
|
}
|
|
|
|
// Now that all specified events have been exhausted we loop until the test finishes
|
|
// and the context bound to the tests lifecycle has been cancelled. This prevents getting
|
|
// any weird errors from the controller manager/runner.
|
|
watchListClient.EXPECT().
|
|
Recv().
|
|
RunAndReturn(func() (*pbresource.WatchEvent, error) {
|
|
<-ctx.Done()
|
|
return nil, ctx.Err()
|
|
}).
|
|
Maybe()
|
|
|
|
return watchListClient
|
|
}
|
|
|
|
// TestControllerRuntimeMemoryCloning mainly is testing that the runtimes
|
|
// provided to reconcilers and dependency mappers will return data from
|
|
// the resource service client and the cache that have been cloned so that
|
|
// the controller should be free to modify the data as needed.
|
|
func TestControllerRuntimeMemoryCloning(t *testing.T) {
|
|
ctx := testutil.TestContext(t)
|
|
|
|
// create some resources to use during the test
|
|
res1 := resourcetest.Resource(fakeType, "foo").
|
|
WithTenancy(resource.DefaultNamespacedTenancy()).
|
|
Build()
|
|
res2 := resourcetest.Resource(fakeV2Type, "bar").
|
|
WithTenancy(resource.DefaultNamespacedTenancy()).
|
|
Build()
|
|
|
|
// create the reconciler that will read the desired resource
|
|
// from both the resource service client and the cache client.
|
|
reconciler := newMemCheckReconciler(t)
|
|
|
|
// Create the v1 watch list client to be returned when the controller runner
|
|
// calls WatchList on the v1 testing type.
|
|
v1WatchListClient := watchListEvents(t, &pbresource.WatchEvent{
|
|
Operation: pbresource.WatchEvent_OPERATION_UPSERT,
|
|
Resource: res1,
|
|
})
|
|
|
|
// Create the v2 watch list client to be returned when the controller runner
|
|
// calls WatchList on the v2 testing type.
|
|
v2WatchListClient := watchListEvents(t, nil, &pbresource.WatchEvent{
|
|
Operation: pbresource.WatchEvent_OPERATION_UPSERT,
|
|
Resource: res2,
|
|
})
|
|
|
|
// Create the mock resource service client
|
|
mres := mockpbresource.NewResourceServiceClient(t)
|
|
// Setup the expectation for the controller runner to issue a WatchList
|
|
// request for the managed type (fake v2 type)
|
|
mres.EXPECT().
|
|
WatchList(mock.Anything, &pbresource.WatchListRequest{
|
|
Type: fakeV2Type,
|
|
Tenancy: &pbresource.Tenancy{
|
|
Partition: storage.Wildcard,
|
|
Namespace: storage.Wildcard,
|
|
},
|
|
}).
|
|
Return(v2WatchListClient, nil).
|
|
Once()
|
|
|
|
// Setup the expectation for the controller runner to issue a WatchList
|
|
// request for the secondary Watch type (fake v1 type)
|
|
mres.EXPECT().
|
|
WatchList(mock.Anything, &pbresource.WatchListRequest{
|
|
Type: fakeType,
|
|
Tenancy: &pbresource.Tenancy{
|
|
Partition: storage.Wildcard,
|
|
Namespace: storage.Wildcard,
|
|
},
|
|
}).
|
|
Return(v1WatchListClient, nil).
|
|
Once()
|
|
|
|
// The cloning resource clients will forward actual calls onto the main resource service client.
|
|
// Here we are configuring the service mock to return either of the resources depending on the
|
|
// id present in the request.
|
|
mres.EXPECT().
|
|
Read(mock.Anything, mock.Anything).
|
|
RunAndReturn(func(_ context.Context, req *pbresource.ReadRequest, opts ...grpc.CallOption) (*pbresource.ReadResponse, error) {
|
|
res := res2
|
|
if resource.EqualID(res1.Id, req.Id) {
|
|
res = res1
|
|
}
|
|
return &pbresource.ReadResponse{Resource: res}, nil
|
|
}).
|
|
Times(0)
|
|
|
|
// create the test controller
|
|
ctl := NewController("test", fakeV2Type).
|
|
WithWatch(fakeType, reconciler.MapToNothing).
|
|
WithReconciler(reconciler)
|
|
|
|
// create the controller manager and register our test controller
|
|
manager := NewManager(mres, testutil.Logger(t))
|
|
manager.Register(ctl)
|
|
|
|
// run the controller manager
|
|
manager.SetRaftLeader(true)
|
|
go manager.Run(ctx)
|
|
|
|
// All future assertions should easily be able to run within 5s although they
|
|
// should typically run a couple orders of magnitude faster.
|
|
timeLimitedCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
|
t.Cleanup(cancel)
|
|
|
|
// validate that the v2 resource type event was seen and that the
|
|
// cache and the resource service client return cloned resources
|
|
reconciler.checkReconcileResult(t, timeLimitedCtx, res2)
|
|
|
|
// Validate that the dependency mapper's resource and cache clients return
|
|
// cloned resources.
|
|
reconciler.checkMapResult(t, timeLimitedCtx, res1)
|
|
}
|
|
|
|
// TestRunnerSharedMemoryCache is mainly testing to ensure that resources
|
|
// within the cache are shared with the resource service and have not been
|
|
// cloned.
|
|
func TestControllerRunnerSharedMemoryCache(t *testing.T) {
|
|
ctx := testutil.TestContext(t)
|
|
|
|
// create resource to use during the test
|
|
res := resourcetest.Resource(fakeV2Type, "bar").
|
|
WithTenancy(resource.DefaultNamespacedTenancy()).
|
|
Build()
|
|
|
|
// create the reconciler that will read the desired resource
|
|
// from both the resource service client and the cache client.
|
|
reconciler := newMemCheckReconciler(t)
|
|
|
|
// Create the v2 watch list client to be returned when the controller runner
|
|
// calls WatchList on the v2 testing type.
|
|
v2WatchListClient := watchListEvents(t, nil, &pbresource.WatchEvent{
|
|
Operation: pbresource.WatchEvent_OPERATION_UPSERT,
|
|
Resource: res,
|
|
})
|
|
|
|
// Create the mock resource service client
|
|
mres := mockpbresource.NewResourceServiceClient(t)
|
|
// Setup the expectation for the controller runner to issue a WatchList
|
|
// request for the managed type (fake v2 type)
|
|
mres.EXPECT().
|
|
WatchList(mock.Anything, &pbresource.WatchListRequest{
|
|
Type: fakeV2Type,
|
|
Tenancy: &pbresource.Tenancy{
|
|
Partition: storage.Wildcard,
|
|
Namespace: storage.Wildcard,
|
|
},
|
|
}).
|
|
Return(v2WatchListClient, nil).
|
|
Once()
|
|
|
|
// The cloning resource clients will forward actual calls onto the main resource service client.
|
|
// Here we are configuring the service mock to return our singular resource always.
|
|
mres.EXPECT().
|
|
Read(mock.Anything, mock.Anything).
|
|
Return(&pbresource.ReadResponse{Resource: res}, nil).
|
|
Times(0)
|
|
|
|
// create the test controller
|
|
ctl := NewController("test", fakeV2Type).
|
|
WithReconciler(reconciler)
|
|
|
|
runner := newControllerRunner(ctl, mres, testutil.Logger(t))
|
|
go runner.run(ctx)
|
|
|
|
// Wait for reconcile to be called before we check the values in
|
|
// the cache. This will also validate that the resource service client
|
|
// and cache client given to the reconciler cloned the resource but
|
|
// that is tested more thoroughly in another test and isn't of primary
|
|
// concern here.
|
|
reconciler.checkReconcileResult(t, ctx, res)
|
|
|
|
// Now validate that the cache hold the same resource pointer as the original data
|
|
actual, err := runner.cache.Get(fakeV2Type, "id", res.Id)
|
|
require.NoError(t, err)
|
|
require.Same(t, res, actual)
|
|
}
|