mirror of https://github.com/status-im/consul.git
resource: adding various helpers for working with resources (#18342)
This is a bit of a grab bag of helpers that I found useful for working with them when authoring substantial Controllers. Subsequent PRs will make use of them.
This commit is contained in:
parent
e459399e39
commit
13ce787a3f
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DecodedResource is a generic holder to contain an original Resource and its
|
||||||
|
// decoded contents.
|
||||||
|
type DecodedResource[V any, PV interface {
|
||||||
|
proto.Message
|
||||||
|
*V
|
||||||
|
}] struct {
|
||||||
|
Resource *pbresource.Resource
|
||||||
|
Data PV
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode will generically decode the provided resource into a 2-field
|
||||||
|
// structure that holds onto the original Resource and the decoded contents.
|
||||||
|
//
|
||||||
|
// Returns an ErrDataParse on unmarshalling errors.
|
||||||
|
func Decode[V any, PV interface {
|
||||||
|
proto.Message
|
||||||
|
*V
|
||||||
|
}](res *pbresource.Resource) (*DecodedResource[V, PV], error) {
|
||||||
|
data := PV(new(V))
|
||||||
|
if err := res.Data.UnmarshalTo(data); err != nil {
|
||||||
|
return nil, NewErrDataParse(data, err)
|
||||||
|
}
|
||||||
|
return &DecodedResource[V, PV]{
|
||||||
|
Resource: res,
|
||||||
|
Data: data,
|
||||||
|
}, nil
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/types/known/anypb"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/internal/resource"
|
||||||
|
"github.com/hashicorp/consul/internal/resource/demo"
|
||||||
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||||
|
pbdemo "github.com/hashicorp/consul/proto/private/pbdemo/v2"
|
||||||
|
"github.com/hashicorp/consul/proto/private/prototest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDecode(t *testing.T) {
|
||||||
|
t.Run("good", func(t *testing.T) {
|
||||||
|
fooData := &pbdemo.Artist{
|
||||||
|
Name: "caspar babypants",
|
||||||
|
}
|
||||||
|
any, err := anypb.New(fooData)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
foo := &pbresource.Resource{
|
||||||
|
Id: &pbresource.ID{
|
||||||
|
Type: demo.TypeV2Artist,
|
||||||
|
Tenancy: demo.TenancyDefault,
|
||||||
|
Name: "babypants",
|
||||||
|
},
|
||||||
|
Data: any,
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"generated_at": time.Now().Format(time.RFC3339),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
dec, err := resource.Decode[pbdemo.Artist, *pbdemo.Artist](foo)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
prototest.AssertDeepEqual(t, foo, dec.Resource)
|
||||||
|
prototest.AssertDeepEqual(t, fooData, dec.Data)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("bad", func(t *testing.T) {
|
||||||
|
foo := &pbresource.Resource{
|
||||||
|
Id: &pbresource.ID{
|
||||||
|
Type: demo.TypeV2Artist,
|
||||||
|
Tenancy: demo.TenancyDefault,
|
||||||
|
Name: "babypants",
|
||||||
|
},
|
||||||
|
Data: &anypb.Any{
|
||||||
|
TypeUrl: "garbage",
|
||||||
|
Value: []byte("more garbage"),
|
||||||
|
},
|
||||||
|
Metadata: map[string]string{
|
||||||
|
"generated_at": time.Now().Format(time.RFC3339),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := resource.Decode[pbdemo.Artist, *pbdemo.Artist](foo)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
|
@ -120,6 +120,22 @@ func EqualReference(a, b *pbresource.Reference) bool {
|
||||||
a.Section == b.Section
|
a.Section == b.Section
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReferenceOrIDMatch compares two references or IDs to see if they both refer
|
||||||
|
// to the same thing.
|
||||||
|
//
|
||||||
|
// Note that this only compares fields that are common between them as
|
||||||
|
// represented by the ReferenceOrID interface and notably ignores the section
|
||||||
|
// field on references and the uid field on ids.
|
||||||
|
func ReferenceOrIDMatch(ref1, ref2 ReferenceOrID) bool {
|
||||||
|
if ref1 == nil || ref2 == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return EqualType(ref1.GetType(), ref2.GetType()) &&
|
||||||
|
EqualTenancy(ref1.GetTenancy(), ref2.GetTenancy()) &&
|
||||||
|
ref1.GetName() == ref2.GetName()
|
||||||
|
}
|
||||||
|
|
||||||
// EqualStatusMap compares two status maps for equality without reflection.
|
// EqualStatusMap compares two status maps for equality without reflection.
|
||||||
func EqualStatusMap(a, b map[string]*pbresource.Status) bool {
|
func EqualStatusMap(a, b map[string]*pbresource.Status) bool {
|
||||||
if len(a) != len(b) {
|
if len(a) != len(b) {
|
||||||
|
|
|
@ -283,6 +283,310 @@ func TestEqualID(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEqualReference(t *testing.T) {
|
||||||
|
t.Run("same pointer", func(t *testing.T) {
|
||||||
|
id := &pbresource.Reference{
|
||||||
|
Type: &pbresource.Type{
|
||||||
|
Group: "demo",
|
||||||
|
GroupVersion: "v2",
|
||||||
|
Kind: "artist",
|
||||||
|
},
|
||||||
|
Tenancy: &pbresource.Tenancy{
|
||||||
|
Partition: "foo",
|
||||||
|
PeerName: "bar",
|
||||||
|
Namespace: "baz",
|
||||||
|
},
|
||||||
|
Name: "qux",
|
||||||
|
Section: "blah",
|
||||||
|
}
|
||||||
|
require.True(t, resource.EqualReference(id, id))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("equal", func(t *testing.T) {
|
||||||
|
a := &pbresource.Reference{
|
||||||
|
Type: &pbresource.Type{
|
||||||
|
Group: "demo",
|
||||||
|
GroupVersion: "v2",
|
||||||
|
Kind: "artist",
|
||||||
|
},
|
||||||
|
Tenancy: &pbresource.Tenancy{
|
||||||
|
Partition: "foo",
|
||||||
|
PeerName: "bar",
|
||||||
|
Namespace: "baz",
|
||||||
|
},
|
||||||
|
Name: "qux",
|
||||||
|
Section: "blah",
|
||||||
|
}
|
||||||
|
b := clone(a)
|
||||||
|
require.True(t, resource.EqualReference(a, b))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nil", func(t *testing.T) {
|
||||||
|
a := &pbresource.Reference{
|
||||||
|
Type: &pbresource.Type{
|
||||||
|
Group: "demo",
|
||||||
|
GroupVersion: "v2",
|
||||||
|
Kind: "artist",
|
||||||
|
},
|
||||||
|
Tenancy: &pbresource.Tenancy{
|
||||||
|
Partition: "foo",
|
||||||
|
PeerName: "bar",
|
||||||
|
Namespace: "baz",
|
||||||
|
},
|
||||||
|
Name: "qux",
|
||||||
|
Section: "blah",
|
||||||
|
}
|
||||||
|
require.False(t, resource.EqualReference(a, nil))
|
||||||
|
require.False(t, resource.EqualReference(nil, a))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("different type", func(t *testing.T) {
|
||||||
|
a := &pbresource.Reference{
|
||||||
|
Type: &pbresource.Type{
|
||||||
|
Group: "demo",
|
||||||
|
GroupVersion: "v2",
|
||||||
|
Kind: "artist",
|
||||||
|
},
|
||||||
|
Tenancy: &pbresource.Tenancy{
|
||||||
|
Partition: "foo",
|
||||||
|
PeerName: "bar",
|
||||||
|
Namespace: "baz",
|
||||||
|
},
|
||||||
|
Name: "qux",
|
||||||
|
Section: "blah",
|
||||||
|
}
|
||||||
|
b := clone(a)
|
||||||
|
b.Type.Kind = "album"
|
||||||
|
require.False(t, resource.EqualReference(a, b))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("different tenancy", func(t *testing.T) {
|
||||||
|
a := &pbresource.Reference{
|
||||||
|
Type: &pbresource.Type{
|
||||||
|
Group: "demo",
|
||||||
|
GroupVersion: "v2",
|
||||||
|
Kind: "artist",
|
||||||
|
},
|
||||||
|
Tenancy: &pbresource.Tenancy{
|
||||||
|
Partition: "foo",
|
||||||
|
PeerName: "bar",
|
||||||
|
Namespace: "baz",
|
||||||
|
},
|
||||||
|
Name: "qux",
|
||||||
|
Section: "blah",
|
||||||
|
}
|
||||||
|
b := clone(a)
|
||||||
|
b.Tenancy.Namespace = "qux"
|
||||||
|
require.False(t, resource.EqualReference(a, b))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("different name", func(t *testing.T) {
|
||||||
|
a := &pbresource.Reference{
|
||||||
|
Type: &pbresource.Type{
|
||||||
|
Group: "demo",
|
||||||
|
GroupVersion: "v2",
|
||||||
|
Kind: "artist",
|
||||||
|
},
|
||||||
|
Tenancy: &pbresource.Tenancy{
|
||||||
|
Partition: "foo",
|
||||||
|
PeerName: "bar",
|
||||||
|
Namespace: "baz",
|
||||||
|
},
|
||||||
|
Name: "qux",
|
||||||
|
Section: "blah",
|
||||||
|
}
|
||||||
|
b := clone(a)
|
||||||
|
b.Name = "boom"
|
||||||
|
require.False(t, resource.EqualReference(a, b))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("different section", func(t *testing.T) {
|
||||||
|
a := &pbresource.Reference{
|
||||||
|
Type: &pbresource.Type{
|
||||||
|
Group: "demo",
|
||||||
|
GroupVersion: "v2",
|
||||||
|
Kind: "artist",
|
||||||
|
},
|
||||||
|
Tenancy: &pbresource.Tenancy{
|
||||||
|
Partition: "foo",
|
||||||
|
PeerName: "bar",
|
||||||
|
Namespace: "baz",
|
||||||
|
},
|
||||||
|
Name: "qux",
|
||||||
|
Section: "blah",
|
||||||
|
}
|
||||||
|
b := clone(a)
|
||||||
|
b.Section = "not-blah"
|
||||||
|
require.False(t, resource.EqualReference(a, b))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReferenceOrIDMatch(t *testing.T) {
|
||||||
|
t.Run("equal", func(t *testing.T) {
|
||||||
|
a := &pbresource.Reference{
|
||||||
|
Type: &pbresource.Type{
|
||||||
|
Group: "demo",
|
||||||
|
GroupVersion: "v2",
|
||||||
|
Kind: "artist",
|
||||||
|
},
|
||||||
|
Tenancy: &pbresource.Tenancy{
|
||||||
|
Partition: "foo",
|
||||||
|
PeerName: "bar",
|
||||||
|
Namespace: "baz",
|
||||||
|
},
|
||||||
|
Name: "qux",
|
||||||
|
Section: "blah",
|
||||||
|
}
|
||||||
|
b := &pbresource.ID{
|
||||||
|
Type: &pbresource.Type{
|
||||||
|
Group: "demo",
|
||||||
|
GroupVersion: "v2",
|
||||||
|
Kind: "artist",
|
||||||
|
},
|
||||||
|
Tenancy: &pbresource.Tenancy{
|
||||||
|
Partition: "foo",
|
||||||
|
PeerName: "bar",
|
||||||
|
Namespace: "baz",
|
||||||
|
},
|
||||||
|
Name: "qux",
|
||||||
|
Uid: ulid.Make().String(),
|
||||||
|
}
|
||||||
|
require.True(t, resource.ReferenceOrIDMatch(a, b))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nil", func(t *testing.T) {
|
||||||
|
a := &pbresource.Reference{
|
||||||
|
Type: &pbresource.Type{
|
||||||
|
Group: "demo",
|
||||||
|
GroupVersion: "v2",
|
||||||
|
Kind: "artist",
|
||||||
|
},
|
||||||
|
Tenancy: &pbresource.Tenancy{
|
||||||
|
Partition: "foo",
|
||||||
|
PeerName: "bar",
|
||||||
|
Namespace: "baz",
|
||||||
|
},
|
||||||
|
Name: "qux",
|
||||||
|
Section: "blah",
|
||||||
|
}
|
||||||
|
b := &pbresource.ID{
|
||||||
|
Type: &pbresource.Type{
|
||||||
|
Group: "demo",
|
||||||
|
GroupVersion: "v2",
|
||||||
|
Kind: "artist",
|
||||||
|
},
|
||||||
|
Tenancy: &pbresource.Tenancy{
|
||||||
|
Partition: "foo",
|
||||||
|
PeerName: "bar",
|
||||||
|
Namespace: "baz",
|
||||||
|
},
|
||||||
|
Name: "qux",
|
||||||
|
Uid: ulid.Make().String(),
|
||||||
|
}
|
||||||
|
require.False(t, resource.ReferenceOrIDMatch(a, nil))
|
||||||
|
require.False(t, resource.ReferenceOrIDMatch(nil, b))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("different type", func(t *testing.T) {
|
||||||
|
a := &pbresource.Reference{
|
||||||
|
Type: &pbresource.Type{
|
||||||
|
Group: "demo",
|
||||||
|
GroupVersion: "v2",
|
||||||
|
Kind: "artist",
|
||||||
|
},
|
||||||
|
Tenancy: &pbresource.Tenancy{
|
||||||
|
Partition: "foo",
|
||||||
|
PeerName: "bar",
|
||||||
|
Namespace: "baz",
|
||||||
|
},
|
||||||
|
Name: "qux",
|
||||||
|
Section: "blah",
|
||||||
|
}
|
||||||
|
b := &pbresource.ID{
|
||||||
|
Type: &pbresource.Type{
|
||||||
|
Group: "demo",
|
||||||
|
GroupVersion: "v2",
|
||||||
|
Kind: "artist",
|
||||||
|
},
|
||||||
|
Tenancy: &pbresource.Tenancy{
|
||||||
|
Partition: "foo",
|
||||||
|
PeerName: "bar",
|
||||||
|
Namespace: "baz",
|
||||||
|
},
|
||||||
|
Name: "qux",
|
||||||
|
Uid: ulid.Make().String(),
|
||||||
|
}
|
||||||
|
b.Type.Kind = "album"
|
||||||
|
require.False(t, resource.ReferenceOrIDMatch(a, b))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("different tenancy", func(t *testing.T) {
|
||||||
|
a := &pbresource.Reference{
|
||||||
|
Type: &pbresource.Type{
|
||||||
|
Group: "demo",
|
||||||
|
GroupVersion: "v2",
|
||||||
|
Kind: "artist",
|
||||||
|
},
|
||||||
|
Tenancy: &pbresource.Tenancy{
|
||||||
|
Partition: "foo",
|
||||||
|
PeerName: "bar",
|
||||||
|
Namespace: "baz",
|
||||||
|
},
|
||||||
|
Name: "qux",
|
||||||
|
Section: "blah",
|
||||||
|
}
|
||||||
|
b := &pbresource.ID{
|
||||||
|
Type: &pbresource.Type{
|
||||||
|
Group: "demo",
|
||||||
|
GroupVersion: "v2",
|
||||||
|
Kind: "artist",
|
||||||
|
},
|
||||||
|
Tenancy: &pbresource.Tenancy{
|
||||||
|
Partition: "foo",
|
||||||
|
PeerName: "bar",
|
||||||
|
Namespace: "baz",
|
||||||
|
},
|
||||||
|
Name: "qux",
|
||||||
|
Uid: ulid.Make().String(),
|
||||||
|
}
|
||||||
|
b.Tenancy.Namespace = "qux"
|
||||||
|
require.False(t, resource.ReferenceOrIDMatch(a, b))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("different name", func(t *testing.T) {
|
||||||
|
a := &pbresource.Reference{
|
||||||
|
Type: &pbresource.Type{
|
||||||
|
Group: "demo",
|
||||||
|
GroupVersion: "v2",
|
||||||
|
Kind: "artist",
|
||||||
|
},
|
||||||
|
Tenancy: &pbresource.Tenancy{
|
||||||
|
Partition: "foo",
|
||||||
|
PeerName: "bar",
|
||||||
|
Namespace: "baz",
|
||||||
|
},
|
||||||
|
Name: "qux",
|
||||||
|
Section: "blah",
|
||||||
|
}
|
||||||
|
b := &pbresource.ID{
|
||||||
|
Type: &pbresource.Type{
|
||||||
|
Group: "demo",
|
||||||
|
GroupVersion: "v2",
|
||||||
|
Kind: "artist",
|
||||||
|
},
|
||||||
|
Tenancy: &pbresource.Tenancy{
|
||||||
|
Partition: "foo",
|
||||||
|
PeerName: "bar",
|
||||||
|
Namespace: "baz",
|
||||||
|
},
|
||||||
|
Name: "qux",
|
||||||
|
Uid: ulid.Make().String(),
|
||||||
|
}
|
||||||
|
b.Name = "boom"
|
||||||
|
require.False(t, resource.ReferenceOrIDMatch(a, b))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func TestEqualStatus(t *testing.T) {
|
func TestEqualStatus(t *testing.T) {
|
||||||
orig := &pbresource.Status{
|
orig := &pbresource.Status{
|
||||||
ObservedGeneration: ulid.Make().String(),
|
ObservedGeneration: ulid.Make().String(),
|
||||||
|
|
|
@ -0,0 +1,212 @@
|
||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package bimapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/internal/controller"
|
||||||
|
"github.com/hashicorp/consul/internal/resource"
|
||||||
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mapper tracks bidirectional lookup for an item that contains references to
|
||||||
|
// other items. For example: an HTTPRoute has many references to Services.
|
||||||
|
//
|
||||||
|
// The primary object is called the "item" and an item has many "links".
|
||||||
|
// Tracking is done on items.
|
||||||
|
type Mapper struct {
|
||||||
|
itemType, linkType *pbresource.Type
|
||||||
|
|
||||||
|
lock sync.Mutex
|
||||||
|
itemToLink map[resource.ReferenceKey]map[resource.ReferenceKey]struct{}
|
||||||
|
linkToItem map[resource.ReferenceKey]map[resource.ReferenceKey]struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a bimapper between the two required provided types.
|
||||||
|
func New(itemType, linkType *pbresource.Type) *Mapper {
|
||||||
|
if itemType == nil {
|
||||||
|
panic("itemType is required")
|
||||||
|
}
|
||||||
|
if linkType == nil {
|
||||||
|
panic("linkType is required")
|
||||||
|
}
|
||||||
|
return &Mapper{
|
||||||
|
itemType: itemType,
|
||||||
|
linkType: linkType,
|
||||||
|
itemToLink: make(map[resource.ReferenceKey]map[resource.ReferenceKey]struct{}),
|
||||||
|
linkToItem: make(map[resource.ReferenceKey]map[resource.ReferenceKey]struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset clears the internal mappings.
|
||||||
|
func (m *Mapper) Reset() {
|
||||||
|
m.lock.Lock()
|
||||||
|
defer m.lock.Unlock()
|
||||||
|
m.itemToLink = make(map[resource.ReferenceKey]map[resource.ReferenceKey]struct{})
|
||||||
|
m.linkToItem = make(map[resource.ReferenceKey]map[resource.ReferenceKey]struct{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsEmpty returns true if the internal structures are empty.
|
||||||
|
func (m *Mapper) IsEmpty() bool {
|
||||||
|
m.lock.Lock()
|
||||||
|
defer m.lock.Unlock()
|
||||||
|
return len(m.itemToLink) == 0 && len(m.linkToItem) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UntrackItem removes tracking for the provided item. The item type MUST match
|
||||||
|
// the type configured for the item.
|
||||||
|
func (m *Mapper) UntrackItem(item *pbresource.ID) {
|
||||||
|
if !resource.EqualType(item.Type, m.itemType) {
|
||||||
|
panic(fmt.Sprintf("expected item type %q got %q",
|
||||||
|
resource.TypeToString(m.itemType),
|
||||||
|
resource.TypeToString(item.Type),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
m.untrackItem(resource.NewReferenceKey(item))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mapper) untrackItem(item resource.ReferenceKey) {
|
||||||
|
m.lock.Lock()
|
||||||
|
defer m.lock.Unlock()
|
||||||
|
m.removeItemLocked(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TrackItem adds tracking for the provided item. The item and link types MUST
|
||||||
|
// match the types configured for the items and links.
|
||||||
|
func (m *Mapper) TrackItem(item *pbresource.ID, links []*pbresource.Reference) {
|
||||||
|
if !resource.EqualType(item.Type, m.itemType) {
|
||||||
|
panic(fmt.Sprintf("expected item type %q got %q",
|
||||||
|
resource.TypeToString(m.itemType),
|
||||||
|
resource.TypeToString(item.Type),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
linksAsKeys := make([]resource.ReferenceKey, 0, len(links))
|
||||||
|
for _, link := range links {
|
||||||
|
if !resource.EqualType(link.Type, m.linkType) {
|
||||||
|
panic(fmt.Sprintf("expected link type %q got %q",
|
||||||
|
resource.TypeToString(m.linkType),
|
||||||
|
resource.TypeToString(link.Type),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
linksAsKeys = append(linksAsKeys, resource.NewReferenceKey(link))
|
||||||
|
}
|
||||||
|
|
||||||
|
m.trackItem(resource.NewReferenceKey(item), linksAsKeys)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mapper) trackItem(item resource.ReferenceKey, links []resource.ReferenceKey) {
|
||||||
|
m.lock.Lock()
|
||||||
|
defer m.lock.Unlock()
|
||||||
|
|
||||||
|
m.removeItemLocked(item)
|
||||||
|
m.addItemLocked(item, links)
|
||||||
|
}
|
||||||
|
|
||||||
|
// you must hold the lock before calling this function
|
||||||
|
func (m *Mapper) removeItemLocked(item resource.ReferenceKey) {
|
||||||
|
for link := range m.itemToLink[item] {
|
||||||
|
delete(m.linkToItem[link], item)
|
||||||
|
if len(m.linkToItem[link]) == 0 {
|
||||||
|
delete(m.linkToItem, link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(m.itemToLink, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
// you must hold the lock before calling this function
|
||||||
|
func (m *Mapper) addItemLocked(item resource.ReferenceKey, links []resource.ReferenceKey) {
|
||||||
|
if m.itemToLink[item] == nil {
|
||||||
|
m.itemToLink[item] = make(map[resource.ReferenceKey]struct{})
|
||||||
|
}
|
||||||
|
for _, link := range links {
|
||||||
|
m.itemToLink[item][link] = struct{}{}
|
||||||
|
|
||||||
|
if m.linkToItem[link] == nil {
|
||||||
|
m.linkToItem[link] = make(map[resource.ReferenceKey]struct{})
|
||||||
|
}
|
||||||
|
m.linkToItem[link][item] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LinksForItem returns references to links related to the requested item.
|
||||||
|
func (m *Mapper) LinksForItem(item *pbresource.ID) []*pbresource.Reference {
|
||||||
|
if !resource.EqualType(item.Type, m.itemType) {
|
||||||
|
panic(fmt.Sprintf("expected item type %q got %q",
|
||||||
|
resource.TypeToString(m.itemType),
|
||||||
|
resource.TypeToString(item.Type),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
m.lock.Lock()
|
||||||
|
defer m.lock.Unlock()
|
||||||
|
|
||||||
|
items, ok := m.linkToItem[resource.NewReferenceKey(item)]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]*pbresource.Reference, 0, len(items))
|
||||||
|
for item := range items {
|
||||||
|
out = append(out, item.ToReference())
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemsForLink returns item ids for items related to the provided link.
|
||||||
|
func (m *Mapper) ItemsForLink(link *pbresource.ID) []*pbresource.ID {
|
||||||
|
if !resource.EqualType(link.Type, m.linkType) {
|
||||||
|
panic(fmt.Sprintf("expected type %q got %q",
|
||||||
|
resource.TypeToString(m.linkType),
|
||||||
|
resource.TypeToString(link.Type),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
return m.itemsByLink(resource.NewReferenceKey(link))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapLink is suitable as a DependencyMapper to map the provided link event to its item.
|
||||||
|
func (m *Mapper) MapLink(_ context.Context, _ controller.Runtime, res *pbresource.Resource) ([]controller.Request, error) {
|
||||||
|
link := res.Id
|
||||||
|
|
||||||
|
if !resource.EqualType(link.Type, m.linkType) {
|
||||||
|
return nil, fmt.Errorf("expected type %q got %q",
|
||||||
|
resource.TypeToString(m.linkType),
|
||||||
|
resource.TypeToString(link.Type),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
itemIDs := m.itemsByLink(resource.NewReferenceKey(link))
|
||||||
|
|
||||||
|
out := make([]controller.Request, 0, len(itemIDs))
|
||||||
|
for _, item := range itemIDs {
|
||||||
|
if !resource.EqualType(item.Type, m.itemType) {
|
||||||
|
return nil, fmt.Errorf("expected type %q got %q",
|
||||||
|
resource.TypeToString(m.itemType),
|
||||||
|
resource.TypeToString(item.Type),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
out = append(out, controller.Request{ID: item})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Mapper) itemsByLink(ref resource.ReferenceKey) []*pbresource.ID {
|
||||||
|
m.lock.Lock()
|
||||||
|
defer m.lock.Unlock()
|
||||||
|
|
||||||
|
items, ok := m.linkToItem[ref]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
out := make([]*pbresource.ID, 0, len(items))
|
||||||
|
for item := range items {
|
||||||
|
out = append(out, item.ToID())
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
|
@ -0,0 +1,169 @@
|
||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package bimapper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/internal/controller"
|
||||||
|
rtest "github.com/hashicorp/consul/internal/resource/resourcetest"
|
||||||
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||||
|
"github.com/hashicorp/consul/proto/private/prototest"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fakeGroupName = "catalog"
|
||||||
|
fakeVersion = "v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
fakeFooType = &pbresource.Type{
|
||||||
|
Group: fakeGroupName,
|
||||||
|
GroupVersion: fakeVersion,
|
||||||
|
Kind: "Foo",
|
||||||
|
}
|
||||||
|
fakeBarType = &pbresource.Type{
|
||||||
|
Group: fakeGroupName,
|
||||||
|
GroupVersion: fakeVersion,
|
||||||
|
Kind: "Bar",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMapper(t *testing.T) {
|
||||||
|
// Create an advance pointer to some services.
|
||||||
|
|
||||||
|
randoSvc := rtest.Resource(fakeBarType, "rando").Build()
|
||||||
|
apiSvc := rtest.Resource(fakeBarType, "api").Build()
|
||||||
|
fooSvc := rtest.Resource(fakeBarType, "foo").Build()
|
||||||
|
barSvc := rtest.Resource(fakeBarType, "bar").Build()
|
||||||
|
wwwSvc := rtest.Resource(fakeBarType, "www").Build()
|
||||||
|
|
||||||
|
fail1 := rtest.Resource(fakeFooType, "api").Build()
|
||||||
|
fail1_refs := []*pbresource.Reference{
|
||||||
|
newRef(fakeBarType, "api"),
|
||||||
|
newRef(fakeBarType, "foo"),
|
||||||
|
newRef(fakeBarType, "bar"),
|
||||||
|
}
|
||||||
|
|
||||||
|
fail2 := rtest.Resource(fakeFooType, "www").Build()
|
||||||
|
fail2_refs := []*pbresource.Reference{
|
||||||
|
newRef(fakeBarType, "www"),
|
||||||
|
newRef(fakeBarType, "foo"),
|
||||||
|
}
|
||||||
|
|
||||||
|
fail1_updated := rtest.Resource(fakeFooType, "api").Build()
|
||||||
|
fail1_updated_refs := []*pbresource.Reference{
|
||||||
|
newRef(fakeBarType, "api"),
|
||||||
|
newRef(fakeBarType, "bar"),
|
||||||
|
}
|
||||||
|
|
||||||
|
m := New(fakeFooType, fakeBarType)
|
||||||
|
|
||||||
|
// Nothing tracked yet so we assume nothing.
|
||||||
|
requireServicesTracked(t, m, randoSvc)
|
||||||
|
requireServicesTracked(t, m, apiSvc)
|
||||||
|
requireServicesTracked(t, m, fooSvc)
|
||||||
|
requireServicesTracked(t, m, barSvc)
|
||||||
|
requireServicesTracked(t, m, wwwSvc)
|
||||||
|
|
||||||
|
// no-ops
|
||||||
|
m.UntrackItem(fail1.Id)
|
||||||
|
|
||||||
|
// still nothing
|
||||||
|
requireServicesTracked(t, m, randoSvc)
|
||||||
|
requireServicesTracked(t, m, apiSvc)
|
||||||
|
requireServicesTracked(t, m, fooSvc)
|
||||||
|
requireServicesTracked(t, m, barSvc)
|
||||||
|
requireServicesTracked(t, m, wwwSvc)
|
||||||
|
|
||||||
|
// Actually insert some data.
|
||||||
|
m.TrackItem(fail1.Id, fail1_refs)
|
||||||
|
|
||||||
|
requireServicesTracked(t, m, randoSvc)
|
||||||
|
requireServicesTracked(t, m, apiSvc, fail1.Id)
|
||||||
|
requireServicesTracked(t, m, fooSvc, fail1.Id)
|
||||||
|
requireServicesTracked(t, m, barSvc, fail1.Id)
|
||||||
|
requireServicesTracked(t, m, wwwSvc)
|
||||||
|
|
||||||
|
// track it again, no change
|
||||||
|
m.TrackItem(fail1.Id, fail1_refs)
|
||||||
|
|
||||||
|
requireServicesTracked(t, m, randoSvc)
|
||||||
|
requireServicesTracked(t, m, apiSvc, fail1.Id)
|
||||||
|
requireServicesTracked(t, m, fooSvc, fail1.Id)
|
||||||
|
requireServicesTracked(t, m, barSvc, fail1.Id)
|
||||||
|
requireServicesTracked(t, m, wwwSvc)
|
||||||
|
|
||||||
|
// track new one that overlaps slightly
|
||||||
|
m.TrackItem(fail2.Id, fail2_refs)
|
||||||
|
|
||||||
|
requireServicesTracked(t, m, randoSvc)
|
||||||
|
requireServicesTracked(t, m, apiSvc, fail1.Id)
|
||||||
|
requireServicesTracked(t, m, fooSvc, fail1.Id, fail2.Id)
|
||||||
|
requireServicesTracked(t, m, barSvc, fail1.Id)
|
||||||
|
requireServicesTracked(t, m, wwwSvc, fail2.Id)
|
||||||
|
|
||||||
|
// update the original to change it
|
||||||
|
m.TrackItem(fail1_updated.Id, fail1_updated_refs)
|
||||||
|
|
||||||
|
requireServicesTracked(t, m, randoSvc)
|
||||||
|
requireServicesTracked(t, m, apiSvc, fail1.Id)
|
||||||
|
requireServicesTracked(t, m, fooSvc, fail2.Id)
|
||||||
|
requireServicesTracked(t, m, barSvc, fail1.Id)
|
||||||
|
requireServicesTracked(t, m, wwwSvc, fail2.Id)
|
||||||
|
|
||||||
|
// delete the original
|
||||||
|
m.UntrackItem(fail1.Id)
|
||||||
|
|
||||||
|
requireServicesTracked(t, m, randoSvc)
|
||||||
|
requireServicesTracked(t, m, apiSvc)
|
||||||
|
requireServicesTracked(t, m, fooSvc, fail2.Id)
|
||||||
|
requireServicesTracked(t, m, barSvc)
|
||||||
|
requireServicesTracked(t, m, wwwSvc, fail2.Id)
|
||||||
|
|
||||||
|
// delete the other one
|
||||||
|
m.UntrackItem(fail2.Id)
|
||||||
|
|
||||||
|
requireServicesTracked(t, m, randoSvc)
|
||||||
|
requireServicesTracked(t, m, apiSvc)
|
||||||
|
requireServicesTracked(t, m, fooSvc)
|
||||||
|
requireServicesTracked(t, m, barSvc)
|
||||||
|
requireServicesTracked(t, m, wwwSvc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func requireServicesTracked(t *testing.T, mapper *Mapper, link *pbresource.Resource, items ...*pbresource.ID) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
reqs, err := mapper.MapLink(
|
||||||
|
context.Background(),
|
||||||
|
controller.Runtime{},
|
||||||
|
link,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, reqs, len(items))
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
prototest.AssertContainsElement(t, reqs, controller.Request{ID: item})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRef(typ *pbresource.Type, name string) *pbresource.Reference {
|
||||||
|
return rtest.Resource(typ, name).Reference("")
|
||||||
|
}
|
||||||
|
|
||||||
|
func newID(typ *pbresource.Type, name string) *pbresource.ID {
|
||||||
|
return rtest.Resource(typ, name).ID()
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultTenancy() *pbresource.Tenancy {
|
||||||
|
return &pbresource.Tenancy{
|
||||||
|
Partition: "default",
|
||||||
|
Namespace: "default",
|
||||||
|
PeerName: "local",
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,3 +14,26 @@ func Reference(id *pbresource.ID, section string) *pbresource.Reference {
|
||||||
Section: section,
|
Section: section,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IDFromReference returns a Reference converted into an ID. NOTE: the UID
|
||||||
|
// field is not populated, and the Section field of a reference is dropped.
|
||||||
|
func IDFromReference(ref *pbresource.Reference) *pbresource.ID {
|
||||||
|
return &pbresource.ID{
|
||||||
|
Type: ref.Type,
|
||||||
|
Tenancy: ref.Tenancy,
|
||||||
|
Name: ref.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReferenceOrID is the common accessors shared by pbresource.Reference and
|
||||||
|
// pbresource.ID.
|
||||||
|
type ReferenceOrID interface {
|
||||||
|
GetType() *pbresource.Type
|
||||||
|
GetTenancy() *pbresource.Tenancy
|
||||||
|
GetName() string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ ReferenceOrID = (*pbresource.ID)(nil)
|
||||||
|
_ ReferenceOrID = (*pbresource.Reference)(nil)
|
||||||
|
)
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReferenceKey is the pointer-free representation of a ReferenceOrID
|
||||||
|
// suitable for a go map key.
|
||||||
|
type ReferenceKey struct {
|
||||||
|
GVK string
|
||||||
|
Partition string // Tenancy.*
|
||||||
|
Namespace string // Tenancy.*
|
||||||
|
PeerName string // Tenancy.*
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns a string representation of the ReferenceKey. This should not
|
||||||
|
// be relied upon nor parsed and is provided just for debugging and logging
|
||||||
|
// reasons.
|
||||||
|
//
|
||||||
|
// This format should be aligned with IDToString and ReferenceToString.
|
||||||
|
func (r ReferenceKey) String() string {
|
||||||
|
return fmt.Sprintf("%s/%s.%s.%s/%s",
|
||||||
|
r.GVK,
|
||||||
|
orDefault(r.Partition, "default"),
|
||||||
|
orDefault(r.PeerName, "local"),
|
||||||
|
orDefault(r.Namespace, "default"),
|
||||||
|
r.Name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReferenceKey) GetTenancy() *pbresource.Tenancy {
|
||||||
|
return &pbresource.Tenancy{
|
||||||
|
Partition: r.Partition,
|
||||||
|
PeerName: r.PeerName,
|
||||||
|
Namespace: r.Namespace,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToReference converts this back into a pbresource.ID.
|
||||||
|
func (r ReferenceKey) ToID() *pbresource.ID {
|
||||||
|
return &pbresource.ID{
|
||||||
|
Type: GVKToType(r.GVK),
|
||||||
|
Tenancy: r.GetTenancy(),
|
||||||
|
Name: r.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToReference converts this back into a pbresource.Reference.
|
||||||
|
func (r ReferenceKey) ToReference() *pbresource.Reference {
|
||||||
|
return &pbresource.Reference{
|
||||||
|
Type: GVKToType(r.GVK),
|
||||||
|
Tenancy: r.GetTenancy(),
|
||||||
|
Name: r.Name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ReferenceKey) GoString() string { return r.String() }
|
||||||
|
|
||||||
|
func NewReferenceKey(refOrID ReferenceOrID) ReferenceKey {
|
||||||
|
return ReferenceKey{
|
||||||
|
GVK: ToGVK(refOrID.GetType()),
|
||||||
|
Partition: orDefault(refOrID.GetTenancy().GetPartition(), "default"),
|
||||||
|
Namespace: orDefault(refOrID.GetTenancy().GetNamespace(), "default"),
|
||||||
|
PeerName: orDefault(refOrID.GetTenancy().GetPeerName(), "local"),
|
||||||
|
Name: refOrID.GetName(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func orDefault(v, def string) string {
|
||||||
|
if v == "" {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
func GVKToType(gvk string) *pbresource.Type {
|
||||||
|
parts := strings.Split(gvk, ".")
|
||||||
|
if len(parts) != 3 {
|
||||||
|
panic("bad gvk")
|
||||||
|
}
|
||||||
|
return &pbresource.Type{
|
||||||
|
Group: parts[0],
|
||||||
|
GroupVersion: parts[1],
|
||||||
|
Kind: parts[2],
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package resource_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/internal/resource"
|
||||||
|
"github.com/hashicorp/consul/internal/resource/demo"
|
||||||
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||||
|
"github.com/hashicorp/consul/proto/private/prototest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestReferenceKey(t *testing.T) {
|
||||||
|
tenancy1 := &pbresource.Tenancy{}
|
||||||
|
tenancy1_actual := defaultTenancy()
|
||||||
|
tenancy2 := &pbresource.Tenancy{
|
||||||
|
Partition: "ap1",
|
||||||
|
Namespace: "ns-billing",
|
||||||
|
PeerName: "peer-dc4",
|
||||||
|
}
|
||||||
|
tenancy3 := &pbresource.Tenancy{
|
||||||
|
Partition: "ap2",
|
||||||
|
Namespace: "ns-intern",
|
||||||
|
PeerName: "peer-sea",
|
||||||
|
}
|
||||||
|
|
||||||
|
res1, err := demo.GenerateV2Artist()
|
||||||
|
require.NoError(t, err)
|
||||||
|
res1.Id.Tenancy = tenancy1
|
||||||
|
|
||||||
|
res2, err := demo.GenerateV2Artist()
|
||||||
|
require.NoError(t, err)
|
||||||
|
res2.Id.Tenancy = tenancy2
|
||||||
|
|
||||||
|
res3, err := demo.GenerateV2Artist()
|
||||||
|
require.NoError(t, err)
|
||||||
|
res3.Id.Tenancy = tenancy3
|
||||||
|
|
||||||
|
id1 := res1.Id
|
||||||
|
id2 := res2.Id
|
||||||
|
id3 := res3.Id
|
||||||
|
|
||||||
|
ref1 := resource.Reference(id1, "")
|
||||||
|
ref2 := resource.Reference(id2, "")
|
||||||
|
ref3 := resource.Reference(id3, "")
|
||||||
|
|
||||||
|
idRK1 := resource.NewReferenceKey(id1)
|
||||||
|
idRK2 := resource.NewReferenceKey(id2)
|
||||||
|
idRK3 := resource.NewReferenceKey(id3)
|
||||||
|
|
||||||
|
refRK1 := resource.NewReferenceKey(ref1)
|
||||||
|
refRK2 := resource.NewReferenceKey(ref2)
|
||||||
|
refRK3 := resource.NewReferenceKey(ref3)
|
||||||
|
|
||||||
|
require.Equal(t, idRK1, refRK1)
|
||||||
|
require.Equal(t, idRK2, refRK2)
|
||||||
|
require.Equal(t, idRK3, refRK3)
|
||||||
|
|
||||||
|
prototest.AssertDeepEqual(t, tenancy1_actual, idRK1.GetTenancy())
|
||||||
|
prototest.AssertDeepEqual(t, tenancy2, idRK2.GetTenancy())
|
||||||
|
prototest.AssertDeepEqual(t, tenancy3, idRK3.GetTenancy())
|
||||||
|
|
||||||
|
// Now that we tested the defaulting, swap out the tenancy in the id so
|
||||||
|
// that the comparisons work.
|
||||||
|
id1.Tenancy = tenancy1_actual
|
||||||
|
ref1.Tenancy = tenancy1_actual
|
||||||
|
|
||||||
|
prototest.AssertDeepEqual(t, id1, idRK1.ToID())
|
||||||
|
prototest.AssertDeepEqual(t, id2, idRK2.ToID())
|
||||||
|
prototest.AssertDeepEqual(t, id3, idRK3.ToID())
|
||||||
|
|
||||||
|
prototest.AssertDeepEqual(t, ref1, refRK1.ToReference())
|
||||||
|
prototest.AssertDeepEqual(t, ref2, refRK2.ToReference())
|
||||||
|
prototest.AssertDeepEqual(t, ref3, refRK3.ToReference())
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultTenancy() *pbresource.Tenancy {
|
||||||
|
return &pbresource.Tenancy{
|
||||||
|
Partition: "default",
|
||||||
|
Namespace: "default",
|
||||||
|
PeerName: "local",
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,11 @@
|
||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package resourcetest
|
package resourcetest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"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"
|
|
||||||
"github.com/oklog/ulid/v2"
|
"github.com/oklog/ulid/v2"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
|
@ -14,6 +13,12 @@ import (
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
"google.golang.org/protobuf/reflect/protoreflect"
|
"google.golang.org/protobuf/reflect/protoreflect"
|
||||||
"google.golang.org/protobuf/types/known/anypb"
|
"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 {
|
type resourceBuilder struct {
|
||||||
|
@ -118,6 +123,10 @@ func (b *resourceBuilder) ID() *pbresource.ID {
|
||||||
return b.resource.Id
|
return b.resource.Id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *resourceBuilder) Reference(section string) *pbresource.Reference {
|
||||||
|
return resource.Reference(b.ID(), section)
|
||||||
|
}
|
||||||
|
|
||||||
func (b *resourceBuilder) Write(t T, client pbresource.ResourceServiceClient) *pbresource.Resource {
|
func (b *resourceBuilder) Write(t T, client pbresource.ResourceServiceClient) *pbresource.Resource {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
@ -136,6 +145,9 @@ func (b *resourceBuilder) Write(t T, client pbresource.ResourceServiceClient) *p
|
||||||
})
|
})
|
||||||
|
|
||||||
if err == nil || res.Id.Uid != "" || status.Code(err) != codes.FailedPrecondition {
|
if err == nil || res.Id.Uid != "" || status.Code(err) != codes.FailedPrecondition {
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("write saw error: %v", err)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package resourcetest
|
package resourcetest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -5,14 +8,15 @@ import (
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"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"
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
"google.golang.org/grpc/status"
|
"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 {
|
type Client struct {
|
||||||
|
@ -157,6 +161,16 @@ func (client *Client) RequireStatusConditionForCurrentGen(t T, id *pbresource.ID
|
||||||
return res
|
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 {
|
func (client *Client) RequireResourceMeta(t T, id *pbresource.ID, key string, value string) *pbresource.Resource {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
@ -196,6 +210,17 @@ func (client *Client) WaitForStatusCondition(t T, id *pbresource.ID, statusKey s
|
||||||
return res
|
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 {
|
func (client *Client) WaitForNewVersion(t T, id *pbresource.ID, version string) *pbresource.Resource {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package resourcetest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/internal/resource"
|
||||||
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MustDecode[V any, PV interface {
|
||||||
|
proto.Message
|
||||||
|
*V
|
||||||
|
}](t *testing.T, res *pbresource.Resource) *resource.DecodedResource[V, PV] {
|
||||||
|
dec, err := resource.Decode[V, PV](res)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return dec
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package resourcetest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/internal/resource"
|
||||||
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ValidateAndNormalize(t *testing.T, registry resource.Registry, res *pbresource.Resource) {
|
||||||
|
typ := res.Id.Type
|
||||||
|
|
||||||
|
typeInfo, ok := registry.Resolve(typ)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("unhandled resource type: %q", resource.ToGVK(typ))
|
||||||
|
}
|
||||||
|
|
||||||
|
if typeInfo.Mutate != nil {
|
||||||
|
require.NoError(t, typeInfo.Mutate(res), "failed to apply type mutation to resource")
|
||||||
|
}
|
||||||
|
|
||||||
|
if typeInfo.Validate != nil {
|
||||||
|
require.NoError(t, typeInfo.Validate(res), "failed to validate resource")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package resource
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/consul/proto-public/pbresource"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IDToString returns a string representation of pbresource.ID. This should not
|
||||||
|
// be relied upon nor parsed and is provided just for debugging and logging
|
||||||
|
// reasons.
|
||||||
|
//
|
||||||
|
// This format should be aligned with ReferenceToString and
|
||||||
|
// (ReferenceKey).String.
|
||||||
|
func IDToString(id *pbresource.ID) string {
|
||||||
|
s := fmt.Sprintf("%s/%s/%s",
|
||||||
|
TypeToString(id.Type),
|
||||||
|
TenancyToString(id.Tenancy),
|
||||||
|
id.Name,
|
||||||
|
)
|
||||||
|
if id.Uid != "" {
|
||||||
|
return s + "?uid=" + id.Uid
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReferenceToString returns a string representation of pbresource.Reference.
|
||||||
|
// This should not be relied upon nor parsed and is provided just for debugging
|
||||||
|
// and logging reasons.
|
||||||
|
//
|
||||||
|
// This format should be aligned with IDToString and (ReferenceKey).String.
|
||||||
|
func ReferenceToString(ref *pbresource.Reference) string {
|
||||||
|
s := fmt.Sprintf("%s/%s/%s",
|
||||||
|
TypeToString(ref.Type),
|
||||||
|
TenancyToString(ref.Tenancy),
|
||||||
|
ref.Name,
|
||||||
|
)
|
||||||
|
|
||||||
|
if ref.Section != "" {
|
||||||
|
return s + "?section=" + ref.Section
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// TenancyToString returns a string representation of pbresource.Tenancy. This
|
||||||
|
// should not be relied upon nor parsed and is provided just for debugging and
|
||||||
|
// logging reasons.
|
||||||
|
func TenancyToString(tenancy *pbresource.Tenancy) string {
|
||||||
|
return fmt.Sprintf("%s.%s.%s", tenancy.Partition, tenancy.PeerName, tenancy.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeToString returns a string representation of pbresource.Type. This should
|
||||||
|
// not be relied upon nor parsed and is provided just for debugging and logging
|
||||||
|
// reasons.
|
||||||
|
func TypeToString(typ *pbresource.Type) string {
|
||||||
|
return ToGVK(typ)
|
||||||
|
}
|
Loading…
Reference in New Issue