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.
consul/internal/resource/demo/demo.go

420 lines
11 KiB

// 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>
1 year ago
// 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",
}
)
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),
},
})
}
// 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) }