mirror of https://github.com/hashicorp/consul
303 lines
8.3 KiB
Go
303 lines
8.3 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package resourcetest
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"math/rand"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/exp/slices"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/metadata"
|
|
"google.golang.org/grpc/status"
|
|
|
|
"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"
|
|
)
|
|
|
|
type Client struct {
|
|
pbresource.ResourceServiceClient
|
|
|
|
timeout time.Duration
|
|
wait time.Duration
|
|
token string
|
|
}
|
|
|
|
func NewClient(client pbresource.ResourceServiceClient) *Client {
|
|
return NewClientWithACLToken(client, "")
|
|
}
|
|
|
|
func NewClientWithACLToken(client pbresource.ResourceServiceClient, token string) *Client {
|
|
return &Client{
|
|
ResourceServiceClient: client,
|
|
timeout: 7 * time.Second,
|
|
wait: 25 * time.Millisecond,
|
|
token: token,
|
|
}
|
|
}
|
|
|
|
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)) {
|
|
t.Helper()
|
|
retryer := &retry.Timer{Timeout: client.timeout, Wait: client.wait}
|
|
retry.RunWith(retryer, t, fn)
|
|
}
|
|
|
|
func (client *Client) PublishResources(t T, resources []*pbresource.Resource) {
|
|
ctx := client.Context(t)
|
|
|
|
// 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.
|
|
rand.Shuffle(len(resources), func(i, j int) {
|
|
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))
|
|
rsp, err := client.Write(ctx, &pbresource.WriteRequest{
|
|
Resource: res,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
id := rsp.Resource.Id
|
|
t.Cleanup(func() {
|
|
client.MustDelete(t, id)
|
|
})
|
|
|
|
// track the number of resources published
|
|
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")
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (client *Client) RequireResourceNotFound(t T, id *pbresource.ID) {
|
|
t.Helper()
|
|
|
|
rsp, err := client.Read(client.Context(t), &pbresource.ReadRequest{Id: id})
|
|
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()
|
|
|
|
rsp, err := client.Read(client.Context(t), &pbresource.ReadRequest{Id: id})
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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) {
|
|
res = client.RequireStatusConditionForCurrentGen(r, id, statusKey, condition)
|
|
})
|
|
|
|
return res
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (client *Client) WaitForDeletion(t T, id *pbresource.ID) {
|
|
t.Helper()
|
|
|
|
client.retry(t, func(r *retry.R) {
|
|
client.RequireResourceNotFound(r, id)
|
|
})
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
func (client *Client) MustDelete(t T, id *pbresource.ID) {
|
|
t.Helper()
|
|
ctx := client.Context(t)
|
|
|
|
client.retry(t, func(r *retry.R) {
|
|
_, 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)
|
|
})
|
|
}
|