2023-08-01 18:39:15 +00:00
// Copyright (c) HashiCorp, Inc.
2023-08-11 13:12:13 +00:00
// SPDX-License-Identifier: BUSL-1.1
2023-08-01 18:39:15 +00:00
2023-06-06 21:09:48 +00:00
package resourcetest
import (
2023-10-06 16:06:12 +00:00
"context"
2023-11-08 15:45:25 +00:00
"flag"
2023-06-16 20:29:50 +00:00
"fmt"
2023-06-06 21:09:48 +00:00
"math/rand"
"time"
"github.com/stretchr/testify/require"
"golang.org/x/exp/slices"
2023-11-08 15:45:25 +00:00
"google.golang.org/grpc"
2023-06-06 21:09:48 +00:00
"google.golang.org/grpc/codes"
2023-10-06 16:06:12 +00:00
"google.golang.org/grpc/metadata"
2023-06-06 21:09:48 +00:00
"google.golang.org/grpc/status"
2023-08-01 18:39:15 +00:00
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/proto-public/pbresource"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/sdk/testutil/retry"
2023-06-06 21:09:48 +00:00
)
2023-11-08 15:45:25 +00:00
type ClientOption func ( * Client )
func WithRNGSeed ( seed int64 ) ClientOption {
return func ( c * Client ) {
c . rng = rand . New ( rand . NewSource ( seed ) )
}
}
func WithRequestDelay ( minMilliseconds int , maxMilliseconds int ) ClientOption {
return func ( c * Client ) {
min := minMilliseconds
max := maxMilliseconds
if max < min {
min = maxMilliseconds
max = minMilliseconds
}
c . requestDelayMin = min
c . requestDelayMax = max
}
}
func WithACLToken ( token string ) ClientOption {
return func ( c * Client ) {
c . token = token
}
}
2023-06-06 21:09:48 +00:00
type Client struct {
pbresource . ResourceServiceClient
timeout time . Duration
wait time . Duration
2023-10-06 16:06:12 +00:00
token string
2023-06-06 21:09:48 +00:00
2023-11-08 15:45:25 +00:00
rng * rand . Rand
requestDelayMin int
requestDelayMax int
2023-10-06 16:06:12 +00:00
}
2023-11-08 15:45:25 +00:00
func NewClient ( client pbresource . ResourceServiceClient , opts ... ClientOption ) * Client {
c := & Client {
2023-06-06 21:09:48 +00:00
ResourceServiceClient : client ,
timeout : 7 * time . Second ,
wait : 25 * time . Millisecond ,
2023-11-08 15:45:25 +00:00
rng : rand . New ( rand . NewSource ( time . Now ( ) . UnixNano ( ) ) ) ,
// arbitrary write delays are opt-in only
requestDelayMin : 0 ,
requestDelayMax : 0 ,
}
for _ , opt := range opts {
opt ( c )
2023-06-06 21:09:48 +00:00
}
2023-11-08 15:45:25 +00:00
return c
}
func NewClientWithACLToken ( client pbresource . ResourceServiceClient , token string ) * Client {
return NewClient ( client , WithACLToken ( token ) )
2023-06-06 21:09:48 +00:00
}
func ( client * Client ) SetRetryerConfig ( timeout time . Duration , wait time . Duration ) {
client . timeout = timeout
client . wait = wait
}
func ( client * Client ) retry ( t T , fn func ( r * retry . R ) ) {
2023-06-16 16:58:53 +00:00
t . Helper ( )
2023-06-06 21:09:48 +00:00
retryer := & retry . Timer { Timeout : client . timeout , Wait : client . wait }
retry . RunWith ( retryer , t , fn )
}
func ( client * Client ) PublishResources ( t T , resources [ ] * pbresource . Resource ) {
2023-10-06 16:06:12 +00:00
ctx := client . Context ( t )
2023-06-16 20:29:50 +00:00
2023-06-06 21:09:48 +00:00
// Randomize the order of insertion. Generally insertion order shouldn't matter as the
// controllers should eventually converge on the desired state. The exception to this
// is that you cannot insert resources with owner refs before the resource they are
// owned by or insert a resource into a non-default tenant before that tenant exists.
2023-11-08 15:45:25 +00:00
client . rng . Shuffle ( len ( resources ) , func ( i , j int ) {
2023-06-06 21:09:48 +00:00
temp := resources [ i ]
resources [ i ] = resources [ j ]
resources [ j ] = temp
} )
// This slice will be used to track the resources actually published each round. When
// a resource with an owner ID is encountered we will not attempt to write but defer it
// to the next round of publishing
var written [ ] * pbresource . ID
for len ( resources ) > 0 {
var left [ ] * pbresource . Resource
published := 0
for _ , res := range resources {
// check that any owner references would be satisfied
if res . Owner != nil {
found := slices . ContainsFunc ( written , func ( id * pbresource . ID ) bool {
return resource . EqualID ( res . Owner , id )
} )
// the owner hasn't yet been published then we cannot publish this resource
if ! found {
left = append ( left , res )
continue
}
}
t . Logf ( "Writing resource %s with type %s" , res . Id . Name , resource . ToGVK ( res . Id . Type ) )
2023-06-16 20:29:50 +00:00
rsp , err := client . Write ( ctx , & pbresource . WriteRequest {
2023-06-06 21:09:48 +00:00
Resource : res ,
} )
require . NoError ( t , err )
2023-06-16 20:29:50 +00:00
id := rsp . Resource . Id
t . Cleanup ( func ( ) {
2023-11-08 15:45:25 +00:00
client . CleanupDelete ( t , id )
2023-06-16 20:29:50 +00:00
} )
// track the number of resources published
2023-06-06 21:09:48 +00:00
published += 1
written = append ( written , res . Id )
}
// the next round only has this subset of resources to attempt writing
resources = left
// if we didn't publish any resources this round then nothing would
// enable us to do so by iterating again so we break to prevent infinite
// loooping.
if published == 0 {
break
}
}
require . Empty ( t , resources , "Could not publish all resources - some resources have invalid owner references" )
}
2023-11-08 15:45:25 +00:00
func ( client * Client ) Write ( ctx context . Context , in * pbresource . WriteRequest , opts ... grpc . CallOption ) ( * pbresource . WriteResponse , error ) {
client . delayRequest ( )
return client . ResourceServiceClient . Write ( ctx , in , opts ... )
}
2023-10-06 16:06:12 +00:00
func ( client * Client ) Context ( t T ) context . Context {
ctx := testutil . TestContext ( t )
if client . token != "" {
md := metadata . New ( map [ string ] string {
"x-consul-token" : client . token ,
} )
ctx = metadata . NewOutgoingContext ( ctx , md )
}
return ctx
}
2023-06-06 21:09:48 +00:00
func ( client * Client ) RequireResourceNotFound ( t T , id * pbresource . ID ) {
t . Helper ( )
2023-10-06 16:06:12 +00:00
rsp , err := client . Read ( client . Context ( t ) , & pbresource . ReadRequest { Id : id } )
2023-06-06 21:09:48 +00:00
require . Error ( t , err )
require . Equal ( t , codes . NotFound , status . Code ( err ) )
require . Nil ( t , rsp )
}
func ( client * Client ) RequireResourceExists ( t T , id * pbresource . ID ) * pbresource . Resource {
t . Helper ( )
2023-10-06 16:06:12 +00:00
rsp , err := client . Read ( client . Context ( t ) , & pbresource . ReadRequest { Id : id } )
2023-06-06 21:09:48 +00:00
require . NoError ( t , err , "error reading %s with type %s" , id . Name , resource . ToGVK ( id . Type ) )
require . NotNil ( t , rsp )
return rsp . Resource
}
func ( client * Client ) RequireVersionUnchanged ( t T , id * pbresource . ID , version string ) * pbresource . Resource {
t . Helper ( )
res := client . RequireResourceExists ( t , id )
RequireVersionUnchanged ( t , res , version )
return res
}
func ( client * Client ) RequireVersionChanged ( t T , id * pbresource . ID , version string ) * pbresource . Resource {
t . Helper ( )
res := client . RequireResourceExists ( t , id )
RequireVersionChanged ( t , res , version )
return res
}
func ( client * Client ) RequireStatusCondition ( t T , id * pbresource . ID , statusKey string , condition * pbresource . Condition ) * pbresource . Resource {
t . Helper ( )
res := client . RequireResourceExists ( t , id )
RequireStatusCondition ( t , res , statusKey , condition )
return res
}
func ( client * Client ) RequireStatusConditionForCurrentGen ( t T , id * pbresource . ID , statusKey string , condition * pbresource . Condition ) * pbresource . Resource {
t . Helper ( )
res := client . RequireResourceExists ( t , id )
RequireStatusConditionForCurrentGen ( t , res , statusKey , condition )
return res
}
2023-08-01 18:39:15 +00:00
func ( client * Client ) RequireStatusConditionsForCurrentGen ( t T , id * pbresource . ID , statusKey string , conditions [ ] * pbresource . Condition ) * pbresource . Resource {
t . Helper ( )
res := client . RequireResourceExists ( t , id )
for _ , condition := range conditions {
RequireStatusConditionForCurrentGen ( t , res , statusKey , condition )
}
return res
}
2023-06-06 21:09:48 +00:00
func ( client * Client ) RequireResourceMeta ( t T , id * pbresource . ID , key string , value string ) * pbresource . Resource {
t . Helper ( )
res := client . RequireResourceExists ( t , id )
RequireResourceMeta ( t , res , key , value )
return res
}
func ( client * Client ) RequireReconciledCurrentGen ( t T , id * pbresource . ID , statusKey string ) * pbresource . Resource {
t . Helper ( )
res := client . RequireResourceExists ( t , id )
RequireReconciledCurrentGen ( t , res , statusKey )
return res
}
func ( client * Client ) WaitForReconciliation ( t T , id * pbresource . ID , statusKey string ) * pbresource . Resource {
t . Helper ( )
var res * pbresource . Resource
client . retry ( t , func ( r * retry . R ) {
res = client . RequireReconciledCurrentGen ( r , id , statusKey )
} )
return res
}
func ( client * Client ) WaitForStatusCondition ( t T , id * pbresource . ID , statusKey string , condition * pbresource . Condition ) * pbresource . Resource {
t . Helper ( )
var res * pbresource . Resource
client . retry ( t , func ( r * retry . R ) {
2023-06-16 16:58:53 +00:00
res = client . RequireStatusConditionForCurrentGen ( r , id , statusKey , condition )
2023-06-06 21:09:48 +00:00
} )
return res
}
2023-11-07 14:06:10 +00:00
func ( client * Client ) WaitForStatusConditionAnyGen ( t T , id * pbresource . ID , statusKey string , condition * pbresource . Condition ) * pbresource . Resource {
t . Helper ( )
var res * pbresource . Resource
client . retry ( t , func ( r * retry . R ) {
res = client . RequireStatusCondition ( r , id , statusKey , condition )
} )
return res
}
2023-08-01 18:39:15 +00:00
func ( client * Client ) WaitForStatusConditions ( t T , id * pbresource . ID , statusKey string , conditions ... * pbresource . Condition ) * pbresource . Resource {
t . Helper ( )
var res * pbresource . Resource
client . retry ( t , func ( r * retry . R ) {
res = client . RequireStatusConditionsForCurrentGen ( r , id , statusKey , conditions )
} )
return res
}
2023-06-06 21:09:48 +00:00
func ( client * Client ) WaitForNewVersion ( t T , id * pbresource . ID , version string ) * pbresource . Resource {
t . Helper ( )
var res * pbresource . Resource
client . retry ( t , func ( r * retry . R ) {
res = client . RequireVersionChanged ( r , id , version )
} )
return res
}
func ( client * Client ) WaitForResourceState ( t T , id * pbresource . ID , verify func ( T , * pbresource . Resource ) ) * pbresource . Resource {
t . Helper ( )
var res * pbresource . Resource
client . retry ( t , func ( r * retry . R ) {
res = client . RequireResourceExists ( r , id )
verify ( r , res )
} )
return res
}
2023-06-16 16:58:53 +00:00
func ( client * Client ) WaitForDeletion ( t T , id * pbresource . ID ) {
t . Helper ( )
client . retry ( t , func ( r * retry . R ) {
client . RequireResourceNotFound ( r , id )
} )
}
2023-06-06 21:09:48 +00:00
// ResolveResourceID will read the specified resource and returns its full ID.
// This is mainly useful to get the ID with the Uid filled out.
func ( client * Client ) ResolveResourceID ( t T , id * pbresource . ID ) * pbresource . ID {
t . Helper ( )
return client . RequireResourceExists ( t , id ) . Id
}
2023-06-16 16:58:53 +00:00
2023-11-08 15:45:25 +00:00
// MustDelete will delete a resource by its id, retrying if necessary and fail the test
// if it cannot delete it within the timeout. The clients request delay settings are
// taken into account with this operation.
2023-06-16 16:58:53 +00:00
func ( client * Client ) MustDelete ( t T , id * pbresource . ID ) {
2023-11-08 15:45:25 +00:00
t . Helper ( )
client . retryDelete ( t , id , true )
}
// CleanupDelete will perform the same operations as MustDelete to ensure the resource is
// deleted. The clients request delay settings are ignored for this operation and it is
// assumed this will only be called in the context of test Cleanup routines where we
// are no longer testing that a controller eventually converges on some values in response
// to the delete.
func ( client * Client ) CleanupDelete ( t T , id * pbresource . ID ) {
t . Helper ( )
client . retryDelete ( t , id , false )
}
func ( client * Client ) retryDelete ( t T , id * pbresource . ID , shouldDelay bool ) {
2023-06-16 20:29:50 +00:00
t . Helper ( )
2023-10-06 16:06:12 +00:00
ctx := client . Context ( t )
2023-06-16 16:58:53 +00:00
2023-06-16 20:29:50 +00:00
client . retry ( t , func ( r * retry . R ) {
2023-11-08 15:45:25 +00:00
if shouldDelay {
client . delayRequest ( )
}
2023-06-16 20:29:50 +00:00
_ , err := client . Delete ( ctx , & pbresource . DeleteRequest { Id : id } )
if status . Code ( err ) == codes . NotFound {
return
}
// codes.Aborted indicates a CAS failure and that the delete request should
// be retried. Anything else should be considered an unrecoverable error.
if err != nil && status . Code ( err ) != codes . Aborted {
r . Stop ( fmt . Errorf ( "failed to delete the resource: %w" , err ) )
return
}
require . NoError ( r , err )
} )
2023-06-16 16:58:53 +00:00
}
2023-11-08 15:45:25 +00:00
func ( client * Client ) delayRequest ( ) {
if client . requestDelayMin == 0 && client . requestDelayMax == 0 {
return
}
var delay time . Duration
if client . requestDelayMin == client . requestDelayMax {
delay = time . Duration ( client . requestDelayMin ) * time . Millisecond
} else {
delay = time . Duration ( client . rng . Intn ( client . requestDelayMax - client . requestDelayMin ) + client . requestDelayMin ) * time . Millisecond
}
time . Sleep ( delay )
}
type CLIOptions struct {
minRequestDelay int
maxRequestDelay int
seed int64
}
type CLIOptionT interface {
Helper ( )
Logf ( string , ... any )
}
func ( o * CLIOptions ) ClientOptions ( t CLIOptionT ) [ ] ClientOption {
t . Helper ( )
t . Logf ( "Using %d for the random number generator seed. Pass -rng-seed=<value> to overwrite the time based seed" , o . seed )
t . Logf ( "Using random request delays between %dms and %dms. Use -min-request-delay=<value> or -max-request-delay=<value> to override the defaults" , o . minRequestDelay , o . maxRequestDelay )
return [ ] ClientOption {
WithRNGSeed ( o . seed ) ,
WithRequestDelay ( o . minRequestDelay , o . maxRequestDelay ) ,
}
}
func ConfigureTestCLIFlags ( ) * CLIOptions {
opts := & CLIOptions {
minRequestDelay : 0 ,
maxRequestDelay : 0 ,
seed : time . Now ( ) . UnixNano ( ) ,
}
flag . Int64Var ( & opts . seed , "rng-seed" , opts . seed , "Seed to use for pseudo-random-number-generators" )
flag . IntVar ( & opts . minRequestDelay , "min-request-delay" , 10 , "Minimum delay before performing a resource write (milliseconds: default=10)" )
flag . IntVar ( & opts . maxRequestDelay , "max-request-delay" , 50 , "Maximum delay before performing a resource write (milliseconds: default=50)" )
return opts
}