mirror of https://github.com/hashicorp/consul
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
437 lines
11 KiB
437 lines
11 KiB
// Copyright (c) HashiCorp, Inc. |
|
// SPDX-License-Identifier: BUSL-1.1 |
|
|
|
// Package demo includes fake resource types for working on Consul's generic |
|
// state storage without having to refer to specific features. |
|
package demo |
|
|
|
import ( |
|
"fmt" |
|
"math/rand" |
|
"strings" |
|
"time" |
|
|
|
"google.golang.org/protobuf/proto" |
|
"google.golang.org/protobuf/types/known/anypb" |
|
|
|
"github.com/hashicorp/consul/acl" |
|
"github.com/hashicorp/consul/internal/resource" |
|
"github.com/hashicorp/consul/proto-public/pbresource" |
|
pbdemov1 "github.com/hashicorp/consul/proto/private/pbdemo/v1" |
|
pbdemov2 "github.com/hashicorp/consul/proto/private/pbdemo/v2" |
|
) |
|
|
|
var ( |
|
// TypeV1Executive represents a a C-suite executive of the company. |
|
// Used as a resource to test cluster scope. |
|
TypeV1Executive = &pbresource.Type{ |
|
Group: "demo", |
|
GroupVersion: "v1", |
|
Kind: "Executive", |
|
} |
|
|
|
// TypeV1RecordLabel represents a record label which artists are signed to. |
|
// Used as a resource to test partiion scope. |
|
TypeV1RecordLabel = &pbresource.Type{ |
|
Group: "demo", |
|
GroupVersion: "v1", |
|
Kind: "RecordLabel", |
|
} |
|
|
|
// TypeV1Artist represents a musician or group of musicians. |
|
TypeV1Artist = &pbresource.Type{ |
|
Group: "demo", |
|
GroupVersion: "v1", |
|
Kind: "Artist", |
|
} |
|
|
|
// TypeV1Album represents a collection of an artist's songs. |
|
TypeV1Album = &pbresource.Type{ |
|
Group: "demo", |
|
GroupVersion: "v1", |
|
Kind: "Album", |
|
} |
|
|
|
// TypeV1Concept represents an abstract concept that can be associated with any other resource. |
|
TypeV1Concept = &pbresource.Type{ |
|
Group: "demo", |
|
GroupVersion: "v1", |
|
Kind: "Concept", |
|
} |
|
|
|
// TypeV2Artist represents a musician or group of musicians. |
|
TypeV2Artist = &pbresource.Type{ |
|
Group: "demo", |
|
GroupVersion: "v2", |
|
Kind: "Artist", |
|
} |
|
|
|
// TypeV2Album represents a collection of an artist's songs. |
|
TypeV2Album = &pbresource.Type{ |
|
Group: "demo", |
|
GroupVersion: "v2", |
|
Kind: "Album", |
|
} |
|
|
|
// TypeV2Festival represents a named collection of artists and genres. |
|
TypeV2Festival = &pbresource.Type{ |
|
Group: "demo", |
|
GroupVersion: "v2", |
|
Kind: "Festival", |
|
} |
|
) |
|
|
|
const ( |
|
ArtistV1ReadPolicy = `key_prefix "resource/demo.v1.Artist/" { policy = "read" }` |
|
ArtistV1WritePolicy = `key_prefix "resource/demo.v1.Artist/" { policy = "write" }` |
|
ArtistV2ReadPolicy = `key_prefix "resource/demo.v2.Artist/" { policy = "read" }` |
|
ArtistV2WritePolicy = `key_prefix "resource/demo.v2.Artist/" { policy = "write" }` |
|
ArtistV2ListPolicy = `key_prefix "resource/" { policy = "list" }` |
|
|
|
ExecutiveV1ReadPolicy = `key_prefix "resource/demo.v1.Executive/" { policy = "read" }` |
|
ExecutiveV1WritePolicy = `key_prefix "resource/demo.v1.Executive/" { policy = "write" }` |
|
|
|
LabelV1ReadPolicy = `key_prefix "resource/demo.v1.Label/" { policy = "read" }` |
|
LabelV1WritePolicy = `key_prefix "resource/demo.v1.Label/" { policy = "write" }` |
|
) |
|
|
|
// RegisterTypes registers the demo types. Should only be called in tests and |
|
// dev mode. |
|
// |
|
// TODO(spatel): We're standing-in key ACLs for demo resources until our ACL |
|
// system can be more modularly extended (or support generic resource permissions). |
|
func RegisterTypes(r resource.Registry) { |
|
readACL := func(authz acl.Authorizer, authzContext *acl.AuthorizerContext, id *pbresource.ID, res *pbresource.Resource) error { |
|
if resource.EqualType(TypeV1RecordLabel, id.Type) { |
|
if res == nil { |
|
return resource.ErrNeedResource |
|
} |
|
} |
|
key := fmt.Sprintf("resource/%s/%s", resource.ToGVK(id.Type), id.Name) |
|
return authz.ToAllowAuthorizer().KeyReadAllowed(key, authzContext) |
|
} |
|
|
|
writeACL := func(authz acl.Authorizer, authzContext *acl.AuthorizerContext, res *pbresource.Resource) error { |
|
key := fmt.Sprintf("resource/%s/%s", resource.ToGVK(res.Id.Type), res.Id.Name) |
|
return authz.ToAllowAuthorizer().KeyWriteAllowed(key, authzContext) |
|
} |
|
|
|
makeListACL := func(typ *pbresource.Type) func(acl.Authorizer, *acl.AuthorizerContext) error { |
|
return func(authz acl.Authorizer, authzContext *acl.AuthorizerContext) error { |
|
key := fmt.Sprintf("resource/%s", resource.ToGVK(typ)) |
|
return authz.ToAllowAuthorizer().KeyListAllowed(key, authzContext) |
|
} |
|
} |
|
|
|
validateV1ArtistFn := func(res *pbresource.Resource) error { |
|
artist := &pbdemov1.Artist{} |
|
if err := anypb.UnmarshalTo(res.Data, artist, proto.UnmarshalOptions{}); err != nil { |
|
return err |
|
} |
|
if artist.Name == "" { |
|
return fmt.Errorf("artist.name required") |
|
} |
|
return nil |
|
} |
|
|
|
validateV2ArtistFn := func(res *pbresource.Resource) error { |
|
artist := &pbdemov2.Artist{} |
|
if err := anypb.UnmarshalTo(res.Data, artist, proto.UnmarshalOptions{}); err != nil { |
|
return err |
|
} |
|
if artist.Name == "" { |
|
return fmt.Errorf("artist.name required") |
|
} |
|
return nil |
|
} |
|
|
|
mutateV2ArtistFn := func(res *pbresource.Resource) error { |
|
// Not a realistic use for this hook, but set genre if not specified |
|
artist := &pbdemov2.Artist{} |
|
if err := anypb.UnmarshalTo(res.Data, artist, proto.UnmarshalOptions{}); err != nil { |
|
return err |
|
} |
|
if artist.Genre == pbdemov2.Genre_GENRE_UNSPECIFIED { |
|
artist.Genre = pbdemov2.Genre_GENRE_DISCO |
|
return res.Data.MarshalFrom(artist) |
|
} |
|
return nil |
|
} |
|
|
|
r.Register(resource.Registration{ |
|
Type: TypeV1Executive, |
|
Proto: &pbdemov1.Executive{}, |
|
Scope: resource.ScopeCluster, |
|
ACLs: &resource.ACLHooks{ |
|
Read: readACL, |
|
Write: writeACL, |
|
List: makeListACL(TypeV1Executive), |
|
}, |
|
}) |
|
|
|
r.Register(resource.Registration{ |
|
Type: TypeV1RecordLabel, |
|
Proto: &pbdemov1.RecordLabel{}, |
|
Scope: resource.ScopePartition, |
|
ACLs: &resource.ACLHooks{ |
|
Read: readACL, |
|
Write: writeACL, |
|
List: makeListACL(TypeV1RecordLabel), |
|
}, |
|
}) |
|
|
|
r.Register(resource.Registration{ |
|
Type: TypeV1Artist, |
|
Proto: &pbdemov1.Artist{}, |
|
Scope: resource.ScopeNamespace, |
|
ACLs: &resource.ACLHooks{ |
|
Read: readACL, |
|
Write: writeACL, |
|
List: makeListACL(TypeV1Artist), |
|
}, |
|
Validate: validateV1ArtistFn, |
|
}) |
|
|
|
r.Register(resource.Registration{ |
|
Type: TypeV1Album, |
|
Proto: &pbdemov1.Album{}, |
|
Scope: resource.ScopeNamespace, |
|
ACLs: &resource.ACLHooks{ |
|
Read: readACL, |
|
Write: writeACL, |
|
List: makeListACL(TypeV1Album), |
|
}, |
|
}) |
|
|
|
r.Register(resource.Registration{ |
|
Type: TypeV1Concept, |
|
Proto: &pbdemov1.Concept{}, |
|
Scope: resource.ScopeNamespace, |
|
ACLs: &resource.ACLHooks{ |
|
Read: readACL, |
|
Write: writeACL, |
|
List: makeListACL(TypeV1Concept), |
|
}, |
|
}) |
|
|
|
r.Register(resource.Registration{ |
|
Type: TypeV2Artist, |
|
Proto: &pbdemov2.Artist{}, |
|
Scope: resource.ScopeNamespace, |
|
ACLs: &resource.ACLHooks{ |
|
Read: readACL, |
|
Write: writeACL, |
|
List: makeListACL(TypeV2Artist), |
|
}, |
|
Validate: validateV2ArtistFn, |
|
Mutate: mutateV2ArtistFn, |
|
}) |
|
|
|
r.Register(resource.Registration{ |
|
Type: TypeV2Album, |
|
Proto: &pbdemov2.Album{}, |
|
Scope: resource.ScopeNamespace, |
|
ACLs: &resource.ACLHooks{ |
|
Read: readACL, |
|
Write: writeACL, |
|
List: makeListACL(TypeV2Album), |
|
}, |
|
}) |
|
|
|
r.Register(resource.Registration{ |
|
Type: TypeV2Festival, |
|
Proto: &pbdemov2.Festival{}, |
|
Scope: resource.ScopeNamespace, |
|
ACLs: &resource.ACLHooks{ |
|
Read: readACL, |
|
Write: writeACL, |
|
List: makeListACL(TypeV2Festival), |
|
}, |
|
}) |
|
} |
|
|
|
// GenerateV1Executive generates a named Executive resource. |
|
func GenerateV1Executive(name, position string) (*pbresource.Resource, error) { |
|
data, err := anypb.New(&pbdemov1.Executive{Position: position}) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
return &pbresource.Resource{ |
|
Id: &pbresource.ID{ |
|
Type: TypeV1Executive, |
|
Tenancy: resource.DefaultClusteredTenancy(), |
|
Name: name, |
|
}, |
|
Data: data, |
|
Metadata: map[string]string{ |
|
"generated_at": time.Now().Format(time.RFC3339), |
|
}, |
|
}, nil |
|
} |
|
|
|
// GenerateV1RecordLabel generates a named RecordLabel resource. |
|
func GenerateV1RecordLabel(name string) (*pbresource.Resource, error) { |
|
data, err := anypb.New(&pbdemov1.RecordLabel{Name: name}) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
return &pbresource.Resource{ |
|
Id: &pbresource.ID{ |
|
Type: TypeV1RecordLabel, |
|
Tenancy: resource.DefaultPartitionedTenancy(), |
|
Name: name, |
|
}, |
|
Data: data, |
|
Metadata: map[string]string{ |
|
"generated_at": time.Now().Format(time.RFC3339), |
|
}, |
|
}, nil |
|
} |
|
|
|
// GenerateV1Concept generates a named concept resource. |
|
func GenerateV1Concept(name string) (*pbresource.Resource, error) { |
|
return &pbresource.Resource{ |
|
Id: &pbresource.ID{ |
|
Type: TypeV1Concept, |
|
Tenancy: resource.DefaultPartitionedTenancy(), |
|
Name: name, |
|
}, |
|
Data: nil, |
|
Metadata: map[string]string{ |
|
"generated_at": time.Now().Format(time.RFC3339), |
|
}, |
|
}, nil |
|
} |
|
|
|
// GenerateV2Artist generates a random Artist resource. |
|
func GenerateV2Artist() (*pbresource.Resource, error) { |
|
adjective := adjectives[rand.Intn(len(adjectives))] |
|
noun := nouns[rand.Intn(len(nouns))] |
|
|
|
numMembers := rand.Intn(5) + 1 |
|
groupMembers := make(map[string]string, numMembers) |
|
for i := 0; i < numMembers; i++ { |
|
groupMembers[members[rand.Intn(len(members))]] = instruments[rand.Intn(len(instruments))] |
|
} |
|
|
|
data, err := anypb.New(&pbdemov2.Artist{ |
|
Name: fmt.Sprintf("%s %s", adjective, noun), |
|
Genre: randomGenre(), |
|
GroupMembers: groupMembers, |
|
}) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
return &pbresource.Resource{ |
|
Id: &pbresource.ID{ |
|
Type: TypeV2Artist, |
|
Tenancy: resource.DefaultNamespacedTenancy(), |
|
Name: fmt.Sprintf("%s-%s", strings.ToLower(adjective), strings.ToLower(noun)), |
|
}, |
|
Data: data, |
|
Metadata: map[string]string{ |
|
"generated_at": time.Now().Format(time.RFC3339), |
|
}, |
|
}, nil |
|
} |
|
|
|
// GenerateV2Album generates a random Album resource, owned by the Artist with |
|
// the given ID. |
|
func GenerateV2Album(artistID *pbresource.ID) (*pbresource.Resource, error) { |
|
return generateV2Album(artistID, rand.New(rand.NewSource(time.Now().UnixNano()))) |
|
} |
|
|
|
func generateV2Album(artistID *pbresource.ID, rand *rand.Rand) (*pbresource.Resource, error) { |
|
adjective := adjectives[rand.Intn(len(adjectives))] |
|
noun := nouns[rand.Intn(len(nouns))] |
|
|
|
numTracks := 3 + rand.Intn(3) |
|
tracks := make([]string, numTracks) |
|
for i := 0; i < numTracks; i++ { |
|
words := nouns |
|
if i%3 == 0 { |
|
words = adjectives |
|
} |
|
tracks[i] = words[rand.Intn(len(words))] |
|
} |
|
|
|
data, err := anypb.New(&pbdemov2.Album{ |
|
Title: fmt.Sprintf("%s %s", adjective, noun), |
|
YearOfRelease: int32(1990 + rand.Intn(time.Now().Year()-1990)), |
|
CriticallyAclaimed: rand.Int()%2 == 0, |
|
Tracks: tracks, |
|
}) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
return &pbresource.Resource{ |
|
Id: &pbresource.ID{ |
|
Type: TypeV2Album, |
|
Tenancy: clone(artistID.Tenancy), |
|
Name: fmt.Sprintf("%s-%s-%s", artistID.Name, strings.ToLower(adjective), strings.ToLower(noun)), |
|
}, |
|
Owner: artistID, |
|
Data: data, |
|
Metadata: map[string]string{ |
|
"generated_at": time.Now().Format(time.RFC3339), |
|
}, |
|
}, nil |
|
} |
|
|
|
func randomGenre() pbdemov2.Genre { |
|
return pbdemov2.Genre(rand.Intn(len(pbdemov1.Genre_name)-1) + 1) |
|
} |
|
|
|
var ( |
|
adjectives = []string{ |
|
"Purple", |
|
"Angry", |
|
"Euphoric", |
|
"Unexpected", |
|
"Cheesy", |
|
"Rancid", |
|
"Pleasant", |
|
"Mumbling", |
|
"Enlightened", |
|
} |
|
|
|
nouns = []string{ |
|
"Speakerphone", |
|
"Fox", |
|
"Guppy", |
|
"Smile", |
|
"Emacs", |
|
"Grapefruit", |
|
"Engineer", |
|
"Basketball", |
|
} |
|
|
|
members = []string{ |
|
"Owl", |
|
"Tiger", |
|
"Beetle", |
|
"Lion", |
|
"Chicken", |
|
"Snake", |
|
"Monkey", |
|
"Kitten", |
|
"Hound", |
|
} |
|
|
|
instruments = []string{ |
|
"Guitar", |
|
"Bass", |
|
"Lead Vocals", |
|
"Backing Vocals", |
|
"Drums", |
|
"Synthesizer", |
|
"Triangle", |
|
"Standing by the stage looking cool", |
|
} |
|
) |
|
|
|
func clone[T proto.Message](v T) T { return proto.Clone(v).(T) }
|
|
|