2023-03-28 22:48:58 +00:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
|
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
|
2023-03-27 19:37:54 +00:00
|
|
|
package resource
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"errors"
|
|
|
|
"io"
|
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/hashicorp/consul/internal/resource"
|
|
|
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
|
|
|
"github.com/hashicorp/consul/proto/private/prototest"
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"google.golang.org/grpc/codes"
|
|
|
|
"google.golang.org/grpc/status"
|
|
|
|
"google.golang.org/protobuf/proto"
|
|
|
|
)
|
|
|
|
|
|
|
|
func TestWatchList_TypeNotFound(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
server := testServer(t)
|
|
|
|
client := testClient(t, server)
|
|
|
|
|
|
|
|
stream, err := client.WatchList(context.Background(), &pbresource.WatchListRequest{
|
|
|
|
Type: typev1,
|
|
|
|
Tenancy: tenancy,
|
|
|
|
NamePrefix: "",
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
rspCh := handleResourceStream(t, stream)
|
|
|
|
|
|
|
|
err = mustGetError(t, rspCh)
|
|
|
|
require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String())
|
|
|
|
require.Contains(t, err.Error(), "resource type mesh/v1/service not registered")
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestWatchList_GroupVersionMatches(t *testing.T) {
|
|
|
|
t.Parallel()
|
|
|
|
server := testServer(t)
|
|
|
|
client := testClient(t, server)
|
|
|
|
server.registry.Register(resource.Registration{Type: typev1})
|
|
|
|
ctx := context.Background()
|
|
|
|
|
|
|
|
// create a watch
|
|
|
|
stream, err := client.WatchList(ctx, &pbresource.WatchListRequest{
|
|
|
|
Type: typev1,
|
|
|
|
Tenancy: tenancy,
|
|
|
|
NamePrefix: "",
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
rspCh := handleResourceStream(t, stream)
|
|
|
|
|
|
|
|
// insert and verify upsert event received
|
|
|
|
r1, err := server.backend.WriteCAS(ctx, resourcev1)
|
|
|
|
require.NoError(t, err)
|
|
|
|
rsp := mustGetResource(t, rspCh)
|
|
|
|
require.Equal(t, pbresource.WatchEvent_OPERATION_UPSERT, rsp.Operation)
|
|
|
|
prototest.AssertDeepEqual(t, r1, rsp.Resource)
|
|
|
|
|
|
|
|
// update and verify upsert event received
|
|
|
|
r2 := clone(r1)
|
|
|
|
r2, err = server.backend.WriteCAS(ctx, r2)
|
|
|
|
require.NoError(t, err)
|
|
|
|
rsp = mustGetResource(t, rspCh)
|
|
|
|
require.Equal(t, pbresource.WatchEvent_OPERATION_UPSERT, rsp.Operation)
|
|
|
|
prototest.AssertDeepEqual(t, r2, rsp.Resource)
|
|
|
|
|
|
|
|
// delete and verify delete event received
|
|
|
|
err = server.backend.DeleteCAS(ctx, r2.Id, r2.Version)
|
|
|
|
require.NoError(t, err)
|
|
|
|
rsp = mustGetResource(t, rspCh)
|
|
|
|
require.Equal(t, pbresource.WatchEvent_OPERATION_DELETE, rsp.Operation)
|
|
|
|
}
|
|
|
|
|
|
|
|
func TestWatchList_GroupVersionMismatch(t *testing.T) {
|
|
|
|
// Given a watch on typev2 that only differs from typev1 by GroupVersion
|
|
|
|
// When a resource of typev1 is created/updated/deleted
|
|
|
|
// Then no watch events should be emitted
|
|
|
|
t.Parallel()
|
|
|
|
server := testServer(t)
|
|
|
|
client := testClient(t, server)
|
|
|
|
ctx := context.Background()
|
|
|
|
server.registry.Register(resource.Registration{Type: typev1})
|
|
|
|
server.registry.Register(resource.Registration{Type: typev2})
|
|
|
|
|
|
|
|
// create a watch for typev2
|
|
|
|
stream, err := client.WatchList(ctx, &pbresource.WatchListRequest{
|
|
|
|
Type: typev2,
|
|
|
|
Tenancy: tenancy,
|
|
|
|
NamePrefix: "",
|
|
|
|
})
|
|
|
|
require.NoError(t, err)
|
|
|
|
rspCh := handleResourceStream(t, stream)
|
|
|
|
|
|
|
|
// insert
|
|
|
|
r1, err := server.backend.WriteCAS(ctx, resourcev1)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// update
|
|
|
|
r2 := clone(r1)
|
|
|
|
r2, err = server.backend.WriteCAS(ctx, r2)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// delete
|
|
|
|
err = server.backend.DeleteCAS(ctx, r2.Id, r2.Version)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
// verify no events received
|
|
|
|
mustGetNoResource(t, rspCh)
|
|
|
|
}
|
|
|
|
|
|
|
|
func mustGetNoResource(t *testing.T, ch <-chan resourceOrError) {
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
select {
|
|
|
|
case rsp := <-ch:
|
|
|
|
require.NoError(t, rsp.err)
|
|
|
|
require.Nil(t, rsp.rsp, "expected nil response with no error")
|
|
|
|
case <-time.After(250 * time.Millisecond):
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func mustGetResource(t *testing.T, ch <-chan resourceOrError) *pbresource.WatchEvent {
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
select {
|
|
|
|
case rsp := <-ch:
|
|
|
|
require.NoError(t, rsp.err)
|
|
|
|
return rsp.rsp
|
|
|
|
case <-time.After(1 * time.Second):
|
|
|
|
t.Fatal("timeout waiting for WatchListResponse")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func mustGetError(t *testing.T, ch <-chan resourceOrError) error {
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
select {
|
|
|
|
case rsp := <-ch:
|
|
|
|
require.Error(t, rsp.err)
|
|
|
|
return rsp.err
|
|
|
|
case <-time.After(2 * time.Second):
|
|
|
|
t.Fatal("timeout waiting for WatchListResponse")
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func handleResourceStream(t *testing.T, stream pbresource.ResourceService_WatchListClient) <-chan resourceOrError {
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
rspCh := make(chan resourceOrError)
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
rsp, err := stream.Recv()
|
|
|
|
if errors.Is(err, io.EOF) ||
|
|
|
|
errors.Is(err, context.Canceled) ||
|
|
|
|
errors.Is(err, context.DeadlineExceeded) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
rspCh <- resourceOrError{
|
|
|
|
rsp: rsp,
|
|
|
|
err: err,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
return rspCh
|
|
|
|
}
|
|
|
|
|
|
|
|
type resourceOrError struct {
|
|
|
|
rsp *pbresource.WatchEvent
|
|
|
|
err error
|
|
|
|
}
|
|
|
|
|
|
|
|
func clone[T proto.Message](v T) T { return proto.Clone(v).(T) }
|