consul/internal/resource/resourcetest/builder.go

226 lines
5.4 KiB
Go
Raw Normal View History

// Copyright (c) HashiCorp, Inc.
[COMPLIANCE] License changes (#18443) * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at <Blog URL>, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 * Update copyright file headers to BUSL-1.1 --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com>
2023-08-11 13:12:13 +00:00
// SPDX-License-Identifier: BUSL-1.1
package resourcetest
import (
"context"
"strings"
"github.com/oklog/ulid/v2"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/anypb"
"github.com/hashicorp/consul/internal/resource"
"github.com/hashicorp/consul/internal/storage"
"github.com/hashicorp/consul/proto-public/pbresource"
"github.com/hashicorp/consul/sdk/testutil"
"github.com/hashicorp/consul/sdk/testutil/retry"
)
type resourceBuilder struct {
resource *pbresource.Resource
statuses map[string]*pbresource.Status
dontCleanup bool
}
func Resource(rtype *pbresource.Type, name string) *resourceBuilder {
return &resourceBuilder{
resource: &pbresource.Resource{
Id: &pbresource.ID{
Type: &pbresource.Type{
Group: rtype.Group,
GroupVersion: rtype.GroupVersion,
Kind: rtype.Kind,
},
Name: name,
},
},
}
}
func ResourceWithTenancy(rtype *pbresource.Type, name string, tenancy *pbresource.Tenancy) *resourceBuilder {
return &resourceBuilder{
resource: &pbresource.Resource{
Id: &pbresource.ID{
Type: &pbresource.Type{
Group: rtype.Group,
GroupVersion: rtype.GroupVersion,
Kind: rtype.Kind,
},
Name: name,
Tenancy: tenancy,
},
},
}
}
func ResourceID(id *pbresource.ID) *resourceBuilder {
return &resourceBuilder{
resource: &pbresource.Resource{
Id: id,
},
}
}
func (b *resourceBuilder) WithTenancy(tenant *pbresource.Tenancy) *resourceBuilder {
b.resource.Id.Tenancy = tenant
return b
}
func (b *resourceBuilder) WithData(t T, data protoreflect.ProtoMessage) *resourceBuilder {
t.Helper()
anyData, err := anypb.New(data)
require.NoError(t, err)
b.resource.Data = anyData
return b
}
func (b *resourceBuilder) WithMeta(key string, value string) *resourceBuilder {
if b.resource.Metadata == nil {
b.resource.Metadata = make(map[string]string)
}
b.resource.Metadata[key] = value
return b
}
func (b *resourceBuilder) WithOwner(id *pbresource.ID) *resourceBuilder {
b.resource.Owner = id
return b
}
func (b *resourceBuilder) WithStatus(key string, status *pbresource.Status) *resourceBuilder {
if b.statuses == nil {
b.statuses = make(map[string]*pbresource.Status)
}
b.statuses[key] = status
return b
}
func (b *resourceBuilder) WithoutCleanup() *resourceBuilder {
b.dontCleanup = true
return b
}
func (b *resourceBuilder) WithGeneration(gen string) *resourceBuilder {
b.resource.Generation = gen
return b
}
func (b *resourceBuilder) Build() *pbresource.Resource {
// clone the resource so we can add on status information
res := proto.Clone(b.resource).(*pbresource.Resource)
// fill in the generation if empty to make it look like
// a real managed resource
if res.Generation == "" {
res.Generation = ulid.Make().String()
}
// Now create the status map
res.Status = make(map[string]*pbresource.Status)
for key, original := range b.statuses {
status := &pbresource.Status{
ObservedGeneration: res.Generation,
Conditions: original.Conditions,
}
res.Status[key] = status
}
return res
}
func (b *resourceBuilder) ID() *pbresource.ID {
return b.resource.Id
}
func (b *resourceBuilder) Reference(section string) *pbresource.Reference {
return resource.Reference(b.ID(), section)
}
func (b *resourceBuilder) ReferenceNoSection() *pbresource.Reference {
return resource.Reference(b.ID(), "")
}
func (b *resourceBuilder) Write(t T, client pbresource.ResourceServiceClient) *pbresource.Resource {
t.Helper()
var ctx context.Context
rtestClient, ok := client.(*Client)
if ok {
ctx = rtestClient.Context(t)
} else {
ctx = testutil.TestContext(t)
rtestClient = NewClient(client)
}
res := b.resource
var rsp *pbresource.WriteResponse
var err error
// Retry any writes where the error is a UID mismatch and the UID was not specified. This is indicative
// of using a follower to rewrite an object who is not perfectly in-sync with the leader.
retry.Run(t, func(r *retry.R) {
rsp, err = client.Write(ctx, &pbresource.WriteRequest{
Resource: res,
})
if err == nil || res.Id.Uid != "" || status.Code(err) != codes.FailedPrecondition {
if err != nil {
t.Logf("write saw error: %v", err)
}
return
}
if strings.Contains(err.Error(), storage.ErrWrongUid.Error()) {
r.Fatalf("resource write failed due to uid mismatch - most likely a transient issue when talking to a non-leader")
} else {
// other errors are unexpected and should cause an immediate failure
r.Stop(err)
}
})
require.NoError(t, err)
require.NotNil(t, rsp)
if !b.dontCleanup {
id := proto.Clone(rsp.Resource.Id).(*pbresource.ID)
id.Uid = ""
t.Cleanup(func() {
rtestClient.CleanupDelete(t, id)
})
}
if len(b.statuses) == 0 {
return rsp.Resource
}
for key, original := range b.statuses {
status := &pbresource.Status{
ObservedGeneration: rsp.Resource.Generation,
Conditions: original.Conditions,
}
_, err := client.WriteStatus(ctx, &pbresource.WriteStatusRequest{
Id: rsp.Resource.Id,
Key: key,
Status: status,
})
require.NoError(t, err)
}
readResp, err := client.Read(ctx, &pbresource.ReadRequest{
Id: rsp.Resource.Id,
})
require.NoError(t, err)
return readResp.Resource
}