diff --git a/agent/consul/server.go b/agent/consul/server.go index 6a3e6a1838..1bd52e9d07 100644 --- a/agent/consul/server.go +++ b/agent/consul/server.go @@ -51,7 +51,7 @@ import ( "github.com/hashicorp/consul/agent/grpc-external/services/connectca" "github.com/hashicorp/consul/agent/grpc-external/services/dataplane" "github.com/hashicorp/consul/agent/grpc-external/services/peerstream" - "github.com/hashicorp/consul/agent/grpc-external/services/resource" + resourcegrpc "github.com/hashicorp/consul/agent/grpc-external/services/resource" "github.com/hashicorp/consul/agent/grpc-external/services/serverdiscovery" agentgrpc "github.com/hashicorp/consul/agent/grpc-internal" "github.com/hashicorp/consul/agent/grpc-internal/services/subscribe" @@ -65,6 +65,8 @@ import ( "github.com/hashicorp/consul/agent/rpc/peering" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/agent/token" + "github.com/hashicorp/consul/internal/resource" + "github.com/hashicorp/consul/internal/resource/demo" "github.com/hashicorp/consul/internal/storage" raftstorage "github.com/hashicorp/consul/internal/storage/raft" "github.com/hashicorp/consul/lib" @@ -1196,7 +1198,6 @@ func (s *Server) setupRPC() error { // Initialize and register services on external gRPC server. func (s *Server) setupExternalGRPC(config *Config, backend storage.Backend, logger hclog.Logger) { - s.externalACLServer = aclgrpc.NewServer(aclgrpc.Config{ ACLsEnabled: s.config.ACLsEnabled, ForwardRPC: func(info structs.RPCInfo, fn func(*grpc.ClientConn) error) (bool, error) { @@ -1261,8 +1262,16 @@ func (s *Server) setupExternalGRPC(config *Config, backend storage.Backend, logg }) s.peerStreamServer.Register(s.externalGRPCServer) - resource.NewServer(resource.Config{ - Backend: backend, + registry := resource.NewRegistry() + + if s.config.DevMode { + demo.Register(registry) + } + + resourcegrpc.NewServer(resourcegrpc.Config{ + Registry: registry, + Backend: backend, + Logger: logger.Named("grpc-api.resource"), }).Register(s.externalGRPCServer) } diff --git a/agent/grpc-external/services/resource/list_test.go b/agent/grpc-external/services/resource/list_test.go index a8fcd8795b..3f907edd8d 100644 --- a/agent/grpc-external/services/resource/list_test.go +++ b/agent/grpc-external/services/resource/list_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/hashicorp/consul/internal/resource" + "github.com/hashicorp/consul/internal/resource/demo" "github.com/hashicorp/consul/internal/storage" "github.com/hashicorp/consul/proto-public/pbresource" "github.com/hashicorp/consul/proto/private/prototest" @@ -25,25 +26,25 @@ func TestList_TypeNotFound(t *testing.T) { client := testClient(t, server) _, err := client.List(context.Background(), &pbresource.ListRequest{ - Type: typev1, - Tenancy: tenancy, + Type: demo.TypeV2Artist, + Tenancy: demo.TenancyDefault, NamePrefix: "", }) require.Error(t, err) require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) - require.Contains(t, err.Error(), "resource type mesh/v1/service not registered") + require.Contains(t, err.Error(), "resource type demo/v2/artist not registered") } func TestList_Empty(t *testing.T) { for desc, tc := range listTestCases() { t.Run(desc, func(t *testing.T) { server := testServer(t) + demo.Register(server.Registry) client := testClient(t, server) - server.registry.Register(resource.Registration{Type: typev1}) rsp, err := client.List(tc.ctx, &pbresource.ListRequest{ - Type: typev1, - Tenancy: tenancy, + Type: demo.TypeV1Artist, + Tenancy: demo.TenancyDefault, NamePrefix: "", }) require.NoError(t, err) @@ -56,26 +57,25 @@ func TestList_Many(t *testing.T) { for desc, tc := range listTestCases() { t.Run(desc, func(t *testing.T) { server := testServer(t) + demo.Register(server.Registry) client := testClient(t, server) - server.registry.Register(resource.Registration{Type: typev1}) resources := make([]*pbresource.Resource, 10) for i := 0; i < len(resources); i++ { - r := &pbresource.Resource{ - Id: &pbresource.ID{ - Uid: fmt.Sprintf("uid%d", i), - Name: fmt.Sprintf("name%d", i), - Type: typev1, - Tenancy: tenancy, - }, - Version: "", - } - server.Backend.WriteCAS(tc.ctx, r) + artist, err := demo.GenerateV2Artist() + require.NoError(t, err) + + // Prevent test flakes if the generated names collide. + artist.Id.Name = fmt.Sprintf("%s-%d", artist.Id.Name, i) + _, err = server.Backend.WriteCAS(tc.ctx, artist) + require.NoError(t, err) + + resources[i] = artist } rsp, err := client.List(tc.ctx, &pbresource.ListRequest{ - Type: typev1, - Tenancy: tenancy, + Type: demo.TypeV2Artist, + Tenancy: demo.TenancyDefault, NamePrefix: "", }) require.NoError(t, err) @@ -88,13 +88,18 @@ func TestList_GroupVersionMismatch(t *testing.T) { for desc, tc := range listTestCases() { t.Run(desc, func(t *testing.T) { server := testServer(t) + demo.Register(server.Registry) client := testClient(t, server) - server.registry.Register(resource.Registration{Type: typev1}) - server.Backend.WriteCAS(tc.ctx, &pbresource.Resource{Id: id2}) + + artist, err := demo.GenerateV2Artist() + require.NoError(t, err) + + _, err = server.Backend.WriteCAS(tc.ctx, artist) + require.NoError(t, err) rsp, err := client.List(tc.ctx, &pbresource.ListRequest{ - Type: typev1, - Tenancy: tenancy, + Type: demo.TypeV1Artist, + Tenancy: artist.Id.Tenancy, NamePrefix: "", }) require.NoError(t, err) @@ -109,18 +114,21 @@ func TestList_VerifyReadConsistencyArg(t *testing.T) { t.Run(desc, func(t *testing.T) { mockBackend := NewMockBackend(t) server := NewServer(Config{ - registry: resource.NewRegistry(), + Registry: resource.NewRegistry(), Backend: mockBackend, }) - server.registry.Register(resource.Registration{Type: typev1}) - resource1 := &pbresource.Resource{Id: id1, Version: "1"} + demo.Register(server.Registry) + + artist, err := demo.GenerateV2Artist() + require.NoError(t, err) + mockBackend.On("List", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything). - Return([]*pbresource.Resource{resource1}, nil) + Return([]*pbresource.Resource{artist}, nil) client := testClient(t, server) - rsp, err := client.List(tc.ctx, &pbresource.ListRequest{Type: typev1, Tenancy: tenancy, NamePrefix: ""}) + rsp, err := client.List(tc.ctx, &pbresource.ListRequest{Type: artist.Id.Type, Tenancy: artist.Id.Tenancy, NamePrefix: ""}) require.NoError(t, err) - prototest.AssertDeepEqual(t, resource1, rsp.Resources[0]) + prototest.AssertDeepEqual(t, artist, rsp.Resources[0]) mockBackend.AssertCalled(t, "List", mock.Anything, tc.consistency, mock.Anything, mock.Anything, mock.Anything) }) } diff --git a/agent/grpc-external/services/resource/read_test.go b/agent/grpc-external/services/resource/read_test.go index 4948df00fb..4c33097a3a 100644 --- a/agent/grpc-external/services/resource/read_test.go +++ b/agent/grpc-external/services/resource/read_test.go @@ -14,29 +14,36 @@ import ( "google.golang.org/grpc/status" "github.com/hashicorp/consul/internal/resource" + "github.com/hashicorp/consul/internal/resource/demo" "github.com/hashicorp/consul/internal/storage" "github.com/hashicorp/consul/proto-public/pbresource" "github.com/hashicorp/consul/proto/private/prototest" ) func TestRead_TypeNotFound(t *testing.T) { - server := NewServer(Config{registry: resource.NewRegistry()}) + server := NewServer(Config{Registry: resource.NewRegistry()}) client := testClient(t, server) - _, err := client.Read(context.Background(), &pbresource.ReadRequest{Id: id1}) + artist, err := demo.GenerateV2Artist() + require.NoError(t, err) + + _, err = client.Read(context.Background(), &pbresource.ReadRequest{Id: artist.Id}) require.Error(t, err) require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) - require.Contains(t, err.Error(), "resource type mesh/v1/service not registered") + require.Contains(t, err.Error(), "resource type demo/v2/artist not registered") } func TestRead_ResourceNotFound(t *testing.T) { for desc, tc := range readTestCases() { t.Run(desc, func(t *testing.T) { server := testServer(t) - server.registry.Register(resource.Registration{Type: typev1}) + demo.Register(server.Registry) client := testClient(t, server) - _, err := client.Read(tc.ctx, &pbresource.ReadRequest{Id: id1}) + artist, err := demo.GenerateV2Artist() + require.NoError(t, err) + + _, err = client.Read(tc.ctx, &pbresource.ReadRequest{Id: artist.Id}) require.Error(t, err) require.Equal(t, codes.NotFound.String(), status.Code(err).String()) require.Contains(t, err.Error(), "resource not found") @@ -48,15 +55,19 @@ func TestRead_GroupVersionMismatch(t *testing.T) { for desc, tc := range readTestCases() { t.Run(desc, func(t *testing.T) { server := testServer(t) - server.registry.Register(resource.Registration{Type: typev1}) - server.registry.Register(resource.Registration{Type: typev2}) + demo.Register(server.Registry) client := testClient(t, server) - resource1 := &pbresource.Resource{Id: id1, Version: ""} - _, err := server.Backend.WriteCAS(tc.ctx, resource1) + artist, err := demo.GenerateV2Artist() require.NoError(t, err) - _, err = client.Read(tc.ctx, &pbresource.ReadRequest{Id: id2}) + _, err = server.Backend.WriteCAS(tc.ctx, artist) + require.NoError(t, err) + + id := clone(artist.Id) + id.Type = demo.TypeV1Artist + + _, err = client.Read(tc.ctx, &pbresource.ReadRequest{Id: id}) require.Error(t, err) require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) require.Contains(t, err.Error(), "resource was requested with GroupVersion") @@ -68,13 +79,16 @@ func TestRead_Success(t *testing.T) { for desc, tc := range readTestCases() { t.Run(desc, func(t *testing.T) { server := testServer(t) - server.registry.Register(resource.Registration{Type: typev1}) + demo.Register(server.Registry) client := testClient(t, server) - resource1 := &pbresource.Resource{Id: id1, Version: ""} - resource1, err := server.Backend.WriteCAS(tc.ctx, resource1) + + artist, err := demo.GenerateV2Artist() require.NoError(t, err) - rsp, err := client.Read(tc.ctx, &pbresource.ReadRequest{Id: id1}) + resource1, err := server.Backend.WriteCAS(tc.ctx, artist) + require.NoError(t, err) + + rsp, err := client.Read(tc.ctx, &pbresource.ReadRequest{Id: artist.Id}) require.NoError(t, err) prototest.AssertDeepEqual(t, resource1, rsp.Resource) }) @@ -87,17 +101,20 @@ func TestRead_VerifyReadConsistencyArg(t *testing.T) { t.Run(desc, func(t *testing.T) { mockBackend := NewMockBackend(t) server := NewServer(Config{ - registry: resource.NewRegistry(), + Registry: resource.NewRegistry(), Backend: mockBackend, }) - server.registry.Register(resource.Registration{Type: typev1}) - resource1 := &pbresource.Resource{Id: id1, Version: "1"} - mockBackend.On("Read", mock.Anything, mock.Anything, mock.Anything).Return(resource1, nil) + demo.Register(server.Registry) + + artist, err := demo.GenerateV2Artist() + require.NoError(t, err) + + mockBackend.On("Read", mock.Anything, mock.Anything, mock.Anything).Return(artist, nil) client := testClient(t, server) - rsp, err := client.Read(tc.ctx, &pbresource.ReadRequest{Id: id1}) + rsp, err := client.Read(tc.ctx, &pbresource.ReadRequest{Id: artist.Id}) require.NoError(t, err) - prototest.AssertDeepEqual(t, resource1, rsp.Resource) + prototest.AssertDeepEqual(t, artist, rsp.Resource) mockBackend.AssertCalled(t, "Read", mock.Anything, tc.consistency, mock.Anything) }) } diff --git a/agent/grpc-external/services/resource/server.go b/agent/grpc-external/services/resource/server.go index a0472756d0..125b2bce07 100644 --- a/agent/grpc-external/services/resource/server.go +++ b/agent/grpc-external/services/resource/server.go @@ -6,10 +6,12 @@ package resource import ( "context" + "github.com/hashicorp/go-hclog" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" "github.com/hashicorp/consul/internal/resource" "github.com/hashicorp/consul/internal/storage" @@ -21,7 +23,8 @@ type Server struct { } type Config struct { - registry Registry + Logger hclog.Logger + Registry Registry // Backend is the storage backend that will be used for resource persistence. Backend Backend @@ -47,11 +50,6 @@ func (s *Server) Register(grpcServer *grpc.Server) { pbresource.RegisterResourceServiceServer(grpcServer, s) } -func (s *Server) Write(ctx context.Context, req *pbresource.WriteRequest) (*pbresource.WriteResponse, error) { - // TODO - return &pbresource.WriteResponse{}, nil -} - func (s *Server) WriteStatus(ctx context.Context, req *pbresource.WriteStatusRequest) (*pbresource.WriteStatusResponse, error) { // TODO return &pbresource.WriteStatusResponse{}, nil @@ -64,7 +62,7 @@ func (s *Server) Delete(ctx context.Context, req *pbresource.DeleteRequest) (*pb //nolint:unparam func (s *Server) resolveType(typ *pbresource.Type) (*resource.Registration, error) { - v, ok := s.registry.Resolve(typ) + v, ok := s.Registry.Resolve(typ) if ok { return &v, nil } @@ -90,3 +88,5 @@ func readConsistencyFrom(ctx context.Context) storage.ReadConsistency { } return storage.EventualConsistency } + +func clone[T proto.Message](v T) T { return proto.Clone(v).(T) } diff --git a/agent/grpc-external/services/resource/server_test.go b/agent/grpc-external/services/resource/server_test.go index 349a42e348..2a9c948141 100644 --- a/agent/grpc-external/services/resource/server_test.go +++ b/agent/grpc-external/services/resource/server_test.go @@ -5,25 +5,21 @@ package resource import ( "context" + "fmt" "testing" "github.com/stretchr/testify/require" "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/anypb" "github.com/hashicorp/consul/agent/grpc-external/testutils" "github.com/hashicorp/consul/internal/resource" "github.com/hashicorp/consul/internal/storage/inmem" "github.com/hashicorp/consul/proto-public/pbresource" + pbdemov2 "github.com/hashicorp/consul/proto/private/pbdemo/v2" + "github.com/hashicorp/consul/sdk/testutil" ) -func TestWrite_TODO(t *testing.T) { - server := testServer(t) - client := testClient(t, server) - resp, err := client.Write(context.Background(), &pbresource.WriteRequest{}) - require.NoError(t, err) - require.NotNil(t, resp) -} - func TestWriteStatus_TODO(t *testing.T) { server := testServer(t) client := testClient(t, server) @@ -47,8 +43,11 @@ func testServer(t *testing.T) *Server { require.NoError(t, err) go backend.Run(testContext(t)) - registry := resource.NewRegistry() - return NewServer(Config{registry: registry, Backend: backend}) + return NewServer(Config{ + Logger: testutil.Logger(t), + Registry: resource.NewRegistry(), + Backend: backend, + }) } func testClient(t *testing.T, server *Server) pbresource.ResourceServiceClient { @@ -67,46 +66,24 @@ func testClient(t *testing.T, server *Server) pbresource.ResourceServiceClient { } func testContext(t *testing.T) context.Context { + t.Helper() + ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) return ctx } -var ( - tenancy = &pbresource.Tenancy{ - Partition: "default", - Namespace: "default", - PeerName: "local", - } - typev1 = &pbresource.Type{ - Group: "mesh", - GroupVersion: "v1", - Kind: "service", - } - typev2 = &pbresource.Type{ - Group: "mesh", - GroupVersion: "v2", - Kind: "service", - } - id1 = &pbresource.ID{ - Uid: "abcd", - Name: "billing", - Type: typev1, - Tenancy: tenancy, - } - id2 = &pbresource.ID{ - Uid: "abcd", - Name: "billing", - Type: typev2, - Tenancy: tenancy, - } - resourcev1 = &pbresource.Resource{ - Id: &pbresource.ID{ - Uid: "someUid", - Name: "someName", - Type: typev1, - Tenancy: tenancy, - }, - Version: "", - } -) +func modifyArtist(t *testing.T, res *pbresource.Resource) *pbresource.Resource { + t.Helper() + + var artist pbdemov2.Artist + require.NoError(t, res.Data.UnmarshalTo(&artist)) + artist.Name = fmt.Sprintf("The artist formerly known as %s", artist.Name) + + data, err := anypb.New(&artist) + require.NoError(t, err) + + res = clone(res) + res.Data = data + return res +} diff --git a/agent/grpc-external/services/resource/watch_test.go b/agent/grpc-external/services/resource/watch_test.go index 76cd2ec711..a355d5a5c9 100644 --- a/agent/grpc-external/services/resource/watch_test.go +++ b/agent/grpc-external/services/resource/watch_test.go @@ -10,14 +10,13 @@ import ( "testing" "time" - "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" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" - "google.golang.org/protobuf/proto" ) func TestWatchList_TypeNotFound(t *testing.T) { @@ -26,8 +25,8 @@ func TestWatchList_TypeNotFound(t *testing.T) { client := testClient(t, server) stream, err := client.WatchList(context.Background(), &pbresource.WatchListRequest{ - Type: typev1, - Tenancy: tenancy, + Type: demo.TypeV2Artist, + Tenancy: demo.TenancyDefault, NamePrefix: "", }) require.NoError(t, err) @@ -35,34 +34,37 @@ func TestWatchList_TypeNotFound(t *testing.T) { err = mustGetError(t, rspCh) require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) - require.Contains(t, err.Error(), "resource type mesh/v1/service not registered") + require.Contains(t, err.Error(), "resource type demo/v2/artist not registered") } func TestWatchList_GroupVersionMatches(t *testing.T) { t.Parallel() server := testServer(t) client := testClient(t, server) - server.registry.Register(resource.Registration{Type: typev1}) + demo.Register(server.Registry) ctx := context.Background() // create a watch stream, err := client.WatchList(ctx, &pbresource.WatchListRequest{ - Type: typev1, - Tenancy: tenancy, + Type: demo.TypeV2Artist, + Tenancy: demo.TenancyDefault, NamePrefix: "", }) require.NoError(t, err) rspCh := handleResourceStream(t, stream) + artist, err := demo.GenerateV2Artist() + require.NoError(t, err) + // insert and verify upsert event received - r1, err := server.Backend.WriteCAS(ctx, resourcev1) + r1, err := server.Backend.WriteCAS(ctx, artist) require.NoError(t, err) rsp := mustGetResource(t, rspCh) require.Equal(t, pbresource.WatchEvent_OPERATION_UPSERT, rsp.Operation) prototest.AssertDeepEqual(t, r1, rsp.Resource) // update and verify upsert event received - r2 := clone(r1) + r2 := modifyArtist(t, r1) r2, err = server.Backend.WriteCAS(ctx, r2) require.NoError(t, err) rsp = mustGetResource(t, rspCh) @@ -77,27 +79,29 @@ func TestWatchList_GroupVersionMatches(t *testing.T) { } func TestWatchList_GroupVersionMismatch(t *testing.T) { - // Given a watch on typev2 that only differs from typev1 by GroupVersion - // When a resource of typev1 is created/updated/deleted + // Given a watch on TypeArtistV1 that only differs from TypeArtistV2 by GroupVersion + // When a resource of TypeArtistV2 is created/updated/deleted // Then no watch events should be emitted t.Parallel() server := testServer(t) + demo.Register(server.Registry) client := testClient(t, server) ctx := context.Background() - server.registry.Register(resource.Registration{Type: typev1}) - server.registry.Register(resource.Registration{Type: typev2}) - // create a watch for typev2 + // create a watch for TypeArtistV1 stream, err := client.WatchList(ctx, &pbresource.WatchListRequest{ - Type: typev2, - Tenancy: tenancy, + Type: demo.TypeV1Artist, + Tenancy: demo.TenancyDefault, NamePrefix: "", }) require.NoError(t, err) rspCh := handleResourceStream(t, stream) + artist, err := demo.GenerateV2Artist() + require.NoError(t, err) + // insert - r1, err := server.Backend.WriteCAS(ctx, resourcev1) + r1, err := server.Backend.WriteCAS(ctx, artist) require.NoError(t, err) // update @@ -176,5 +180,3 @@ type resourceOrError struct { rsp *pbresource.WatchEvent err error } - -func clone[T proto.Message](v T) T { return proto.Clone(v).(T) } diff --git a/agent/grpc-external/services/resource/write.go b/agent/grpc-external/services/resource/write.go new file mode 100644 index 0000000000..f207cb5728 --- /dev/null +++ b/agent/grpc-external/services/resource/write.go @@ -0,0 +1,185 @@ +package resource + +import ( + "context" + "errors" + "strings" + "time" + + "github.com/oklog/ulid/v2" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/hashicorp/consul/internal/storage" + "github.com/hashicorp/consul/lib/retry" + "github.com/hashicorp/consul/proto-public/pbresource" +) + +func (s *Server) Write(ctx context.Context, req *pbresource.WriteRequest) (*pbresource.WriteResponse, error) { + if err := validateWriteRequiredFields(req); err != nil { + return nil, err + } + + reg, err := s.resolveType(req.Resource.Id.Type) + if err != nil { + return nil, err + } + + // Check the user sent the correct type of data. + if !req.Resource.Data.MessageIs(reg.Proto) { + got := strings.TrimPrefix(req.Resource.Data.TypeUrl, "type.googleapis.com/") + + return nil, status.Errorf( + codes.InvalidArgument, + "resource.data is of wrong type (expected=%q, got=%q)", + reg.Proto.ProtoReflect().Descriptor().FullName(), + got, + ) + } + + // At the storage backend layer, all writes are CAS operations. + // + // This makes it possible to *safely* do things like keeping the Uid stable + // across writes, carrying statuses over, and passing the current version of + // the resource to hooks, without restricting ourselves to only using the more + // feature-rich storage systems that support "patch" updates etc. natively. + // + // Although CAS semantics are useful for machine users like controllers, human + // users generally don't need them. If the user is performing a non-CAS write, + // we read the current version, and automatically retry if the CAS write fails. + var result *pbresource.Resource + err = s.retryCAS(ctx, req.Resource.Version, func() error { + input := clone(req.Resource) + + // We read with EventualConsistency here because: + // + // - In the common case, individual resources are written infrequently, and + // when using the Raft backend followers are generally within a few hundred + // milliseconds of the leader, so the first read will probably return the + // current version. + // + // - StrongConsistency is expensive. In the Raft backend, it involves a round + // of heartbeats to verify cluster leadership (in addition to the write's + // log replication). + // + // - CAS failures will be retried by retryCAS anyway. So the read-modify-write + // cycle should eventually succeed. + existing, err := s.Backend.Read(ctx, storage.EventualConsistency, input.Id) + switch { + // Create path. + case errors.Is(err, storage.ErrNotFound): + input.Id.Uid = ulid.Make().String() + + // TODO: Prevent setting statuses in this endpoint. + + // Update path. + case err == nil: + // Use the stored ID because it includes the Uid. + // + // Generally, users won't provide the Uid but controllers will, because + // controllers need to operate on a specific "incarnation" of a resource + // as opposed to an older/newer resource with the same name, whereas users + // just want to update the current resource. + input.Id = existing.Id + + // User is doing a non-CAS write, use the current version. + if input.Version == "" { + input.Version = existing.Version + } + + // Check the stored version matches the user-given version. + // + // Although CAS operations are implemented "for real" at the storage backend + // layer, we must check the version here too to prevent a scenario where: + // + // - Current resource version is `v2` + // - User passes version `v2` + // - Read returns stale version `v1` + // - We carry `v1`'s statuses over (effectively overwriting `v2`'s statuses) + // - CAS operation succeeds anyway because user-given version is current + // + // TODO(boxofrad): add a test for this once the status field has been added. + if input.Version != existing.Version { + return storage.ErrCASFailure + } + + // TODO: Carry over the statuses here. + + default: + return err + } + + input.Generation = ulid.Make().String() + result, err = s.Backend.WriteCAS(ctx, input) + return err + }) + + switch { + case errors.Is(err, storage.ErrCASFailure): + return nil, status.Error(codes.Aborted, err.Error()) + case errors.Is(err, storage.ErrWrongUid): + return nil, status.Error(codes.FailedPrecondition, err.Error()) + case err != nil: + return nil, status.Errorf(codes.Internal, "failed to write resource: %v", err.Error()) + } + + return &pbresource.WriteResponse{Resource: result}, nil +} + +// retryCAS retries the given operation with exponential backoff if the user +// didn't provide a version. This is intended to hide failures when the user +// isn't intentionally performing a CAS operation (all writes are, by design, +// CAS operations at the storage backend layer). +func (s *Server) retryCAS(ctx context.Context, vsn string, cas func() error) error { + if vsn != "" { + return cas() + } + + const maxAttempts = 5 + + // These parameters are fairly arbitrary, so if you find better ones then go + // ahead and swap them out! In general, we want to wait long enough to smooth + // over small amounts of storage replication lag, but not so long that we make + // matters worse by holding onto load. + backoff := &retry.Waiter{ + MinWait: 50 * time.Millisecond, + MaxWait: 1 * time.Second, + Jitter: retry.NewJitter(50), + Factor: 75 * time.Millisecond, + } + + var err error + for i := 1; i <= maxAttempts; i++ { + if err = cas(); !errors.Is(err, storage.ErrCASFailure) { + break + } + if backoff.Wait(ctx) != nil { + break + } + s.Logger.Trace("retrying failed CAS operation", "failure_count", i) + } + return err +} + +func validateWriteRequiredFields(req *pbresource.WriteRequest) error { + var field string + switch { + case req.Resource == nil: + field = "resource" + case req.Resource.Id == nil: + field = "resource.id" + case req.Resource.Id.Type == nil: + field = "resource.id.type" + case req.Resource.Id.Tenancy == nil: + field = "resource.id.tenancy" + case req.Resource.Id.Name == "": + field = "resource.id.name" + case req.Resource.Data == nil: + field = "resource.data" + } + + if field == "" { + return nil + } + return status.Errorf(codes.InvalidArgument, "%s is required", field) +} diff --git a/agent/grpc-external/services/resource/write_test.go b/agent/grpc-external/services/resource/write_test.go new file mode 100644 index 0000000000..7b0b324cb9 --- /dev/null +++ b/agent/grpc-external/services/resource/write_test.go @@ -0,0 +1,250 @@ +package resource + +import ( + "context" + "sync" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/anypb" + + "github.com/hashicorp/consul/internal/resource/demo" + "github.com/hashicorp/consul/internal/storage" + "github.com/hashicorp/consul/proto-public/pbresource" + pbdemov2 "github.com/hashicorp/consul/proto/private/pbdemo/v2" +) + +func TestWrite_InputValidation(t *testing.T) { + server := testServer(t) + client := testClient(t, server) + + demo.Register(server.Registry) + + testCases := map[string]func(*pbresource.WriteRequest){ + "no resource": func(req *pbresource.WriteRequest) { req.Resource = nil }, + "no id": func(req *pbresource.WriteRequest) { req.Resource.Id = nil }, + "no type": func(req *pbresource.WriteRequest) { req.Resource.Id.Type = nil }, + "no tenancy": func(req *pbresource.WriteRequest) { req.Resource.Id.Tenancy = nil }, + "no name": func(req *pbresource.WriteRequest) { req.Resource.Id.Name = "" }, + "no data": func(req *pbresource.WriteRequest) { req.Resource.Data = nil }, + "wrong data type": func(req *pbresource.WriteRequest) { + var err error + req.Resource.Data, err = anypb.New(&pbdemov2.Album{}) + require.NoError(t, err) + }, + } + for desc, modFn := range testCases { + t.Run(desc, func(t *testing.T) { + res, err := demo.GenerateV2Artist() + require.NoError(t, err) + + req := &pbresource.WriteRequest{Resource: res} + modFn(req) + + _, err = client.Write(testContext(t), req) + require.Error(t, err) + require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) + }) + } +} + +func TestWrite_TypeNotFound(t *testing.T) { + server := testServer(t) + client := testClient(t, server) + + res, err := demo.GenerateV2Artist() + require.NoError(t, err) + + _, err = client.Write(testContext(t), &pbresource.WriteRequest{Resource: res}) + require.Error(t, err) + require.Equal(t, codes.InvalidArgument.String(), status.Code(err).String()) + require.Contains(t, err.Error(), "resource type demo/v2/artist not registered") +} + +func TestWrite_ResourceCreation(t *testing.T) { + server := testServer(t) + client := testClient(t, server) + + demo.Register(server.Registry) + + res, err := demo.GenerateV2Artist() + require.NoError(t, err) + + rsp, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res}) + require.NoError(t, err) + require.NotEmpty(t, rsp.Resource.Version, "resource should have version") + require.NotEmpty(t, rsp.Resource.Id.Uid, "resource id should have uid") + require.NotEmpty(t, rsp.Resource.Generation, "resource should have generation") +} + +func TestWrite_CASUpdate_Success(t *testing.T) { + server := testServer(t) + client := testClient(t, server) + + demo.Register(server.Registry) + + res, err := demo.GenerateV2Artist() + require.NoError(t, err) + + rsp1, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res}) + require.NoError(t, err) + + rsp2, err := client.Write(testContext(t), &pbresource.WriteRequest{ + Resource: modifyArtist(t, rsp1.Resource), + }) + require.NoError(t, err) + + require.Equal(t, rsp1.Resource.Id.Uid, rsp2.Resource.Id.Uid) + require.NotEqual(t, rsp1.Resource.Version, rsp2.Resource.Version) + require.NotEqual(t, rsp1.Resource.Generation, rsp2.Resource.Generation) +} + +func TestWrite_CASUpdate_Failure(t *testing.T) { + server := testServer(t) + client := testClient(t, server) + + demo.Register(server.Registry) + + res, err := demo.GenerateV2Artist() + require.NoError(t, err) + + rsp1, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res}) + require.NoError(t, err) + + res = modifyArtist(t, rsp1.Resource) + res.Version = "wrong-version" + + _, err = client.Write(testContext(t), &pbresource.WriteRequest{Resource: res}) + require.Error(t, err) + require.Equal(t, codes.Aborted.String(), status.Code(err).String()) + require.Contains(t, err.Error(), "CAS operation failed") +} + +func TestWrite_Update_WrongUid(t *testing.T) { + server := testServer(t) + client := testClient(t, server) + + demo.Register(server.Registry) + + res, err := demo.GenerateV2Artist() + require.NoError(t, err) + + rsp1, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res}) + require.NoError(t, err) + + res = modifyArtist(t, rsp1.Resource) + res.Id.Uid = "wrong-uid" + + _, err = client.Write(testContext(t), &pbresource.WriteRequest{Resource: res}) + require.Error(t, err) + require.Equal(t, codes.FailedPrecondition.String(), status.Code(err).String()) + require.Contains(t, err.Error(), "uid doesn't match") +} + +func TestWrite_Update_NoUid(t *testing.T) { + server := testServer(t) + client := testClient(t, server) + + demo.Register(server.Registry) + + res, err := demo.GenerateV2Artist() + require.NoError(t, err) + + rsp1, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res}) + require.NoError(t, err) + + res = modifyArtist(t, rsp1.Resource) + res.Id.Uid = "" + + _, err = client.Write(testContext(t), &pbresource.WriteRequest{Resource: res}) + require.NoError(t, err) +} + +func TestWrite_NonCASUpdate_Success(t *testing.T) { + server := testServer(t) + client := testClient(t, server) + + demo.Register(server.Registry) + + res, err := demo.GenerateV2Artist() + require.NoError(t, err) + + rsp1, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res}) + require.NoError(t, err) + + res = modifyArtist(t, rsp1.Resource) + res.Version = "" + + rsp2, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res}) + require.NoError(t, err) + require.NotEmpty(t, rsp2.Resource.Version) + require.NotEqual(t, rsp1.Resource.Version, rsp2.Resource.Version) +} + +func TestWrite_NonCASUpdate_Retry(t *testing.T) { + server := testServer(t) + client := testClient(t, server) + + demo.Register(server.Registry) + + res, err := demo.GenerateV2Artist() + require.NoError(t, err) + + rsp1, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res}) + require.NoError(t, err) + + // Simulate conflicting writes by blocking the RPC after it has read the + // current version of the resource, but before it tries to make a write. + backend := &blockOnceBackend{ + Backend: server.Backend, + + readCh: make(chan struct{}), + blockCh: make(chan struct{}), + } + server.Backend = backend + + errCh := make(chan error) + go func() { + res := modifyArtist(t, rsp1.Resource) + res.Version = "" + + _, err := client.Write(testContext(t), &pbresource.WriteRequest{Resource: res}) + errCh <- err + }() + + // Wait for the read, to ensure the Write in the goroutine above has read the + // current version of the resource. + <-backend.readCh + + // Update the resource. + res = modifyArtist(t, rsp1.Resource) + _, err = backend.WriteCAS(testContext(t), res) + require.NoError(t, err) + + // Unblock the read. + close(backend.blockCh) + + // Check that the write succeeded anyway because of a retry. + require.NoError(t, <-errCh) +} + +type blockOnceBackend struct { + storage.Backend + + once sync.Once + readCh chan struct{} + blockCh chan struct{} +} + +func (b *blockOnceBackend) Read(ctx context.Context, consistency storage.ReadConsistency, id *pbresource.ID) (*pbresource.Resource, error) { + res, err := b.Backend.Read(ctx, consistency, id) + + b.once.Do(func() { + close(b.readCh) + <-b.blockCh + }) + + return res, err +} diff --git a/go.mod b/go.mod index 4b76c79a41..5d939ed0d0 100644 --- a/go.mod +++ b/go.mod @@ -85,6 +85,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/pointerstructure v1.2.1 github.com/mitchellh/reflectwalk v1.0.2 + github.com/oklog/ulid/v2 v2.1.0 github.com/olekukonko/tablewriter v0.0.4 github.com/patrickmn/go-cache v2.1.0+incompatible github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index 4667b82470..3ad99625b3 100644 --- a/go.sum +++ b/go.sum @@ -821,6 +821,8 @@ github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= +github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.0-20180130162743-b8a9be070da4/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= @@ -850,6 +852,7 @@ github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0Mw github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= diff --git a/internal/resource/demo/demo.go b/internal/resource/demo/demo.go new file mode 100644 index 0000000000..7d918f18ac --- /dev/null +++ b/internal/resource/demo/demo.go @@ -0,0 +1,202 @@ +// 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/types/known/anypb" + + "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 ( + // TenancyDefault contains the default values for all tenancy units. + TenancyDefault = &pbresource.Tenancy{ + Partition: "default", + PeerName: "local", + Namespace: "default", + } + + // 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", + } + + // 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", + } +) + +// Register demo types. Should only be called in tests and dev mode. +func Register(r resource.Registry) { + r.Register(resource.Registration{ + Type: TypeV1Artist, + Proto: &pbdemov1.Artist{}, + }) + + r.Register(resource.Registration{ + Type: TypeV1Album, + Proto: &pbdemov1.Album{}, + }) + + r.Register(resource.Registration{ + Type: TypeV2Artist, + Proto: &pbdemov2.Artist{}, + }) + + r.Register(resource.Registration{ + Type: TypeV2Album, + Proto: &pbdemov2.Album{}, + }) +} + +// 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: TenancyDefault, + 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) { + 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: 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", + } +) diff --git a/internal/resource/registry.go b/internal/resource/registry.go index db5a8bf4ed..46d2d2c792 100644 --- a/internal/resource/registry.go +++ b/internal/resource/registry.go @@ -7,6 +7,8 @@ import ( "fmt" "sync" + "google.golang.org/protobuf/proto" + "github.com/hashicorp/consul/proto-public/pbresource" ) @@ -22,6 +24,9 @@ type Registration struct { // Type is the GVK of the resource type. Type *pbresource.Type + // Proto is the resource's protobuf message type. + Proto proto.Message + // In the future, we'll add hooks, the controller etc. here. // TODO: https://github.com/hashicorp/consul/pull/16622#discussion_r1134515909 } diff --git a/internal/storage/inmem/store.go b/internal/storage/inmem/store.go index ab3de4a554..4778ed153c 100644 --- a/internal/storage/inmem/store.go +++ b/internal/storage/inmem/store.go @@ -120,15 +120,15 @@ func (s *Store) WriteCAS(res *pbresource.Resource, vsn string) error { if existing != nil { existingRes := existing.(*pbresource.Resource) - // Ensure CAS semantics. - if existingRes.Version != vsn { - return storage.ErrCASFailure - } - // Uid is immutable. if existingRes.Id.Uid != res.Id.Uid { return storage.ErrWrongUid } + + // Ensure CAS semantics. + if existingRes.Version != vsn { + return storage.ErrCASFailure + } } if err := tx.Insert(tableNameResources, res); err != nil { diff --git a/proto/private/pbdemo/v1/demo.pb.binary.go b/proto/private/pbdemo/v1/demo.pb.binary.go new file mode 100644 index 0000000000..45a3c34e55 --- /dev/null +++ b/proto/private/pbdemo/v1/demo.pb.binary.go @@ -0,0 +1,28 @@ +// Code generated by protoc-gen-go-binary. DO NOT EDIT. +// source: private/pbdemo/v1/demo.proto + +package demov1 + +import ( + "google.golang.org/protobuf/proto" +) + +// MarshalBinary implements encoding.BinaryMarshaler +func (msg *Artist) MarshalBinary() ([]byte, error) { + return proto.Marshal(msg) +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler +func (msg *Artist) UnmarshalBinary(b []byte) error { + return proto.Unmarshal(b, msg) +} + +// MarshalBinary implements encoding.BinaryMarshaler +func (msg *Album) MarshalBinary() ([]byte, error) { + return proto.Marshal(msg) +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler +func (msg *Album) UnmarshalBinary(b []byte) error { + return proto.Unmarshal(b, msg) +} diff --git a/proto/private/pbdemo/v1/demo.pb.go b/proto/private/pbdemo/v1/demo.pb.go new file mode 100644 index 0000000000..f163f6debf --- /dev/null +++ b/proto/private/pbdemo/v1/demo.pb.go @@ -0,0 +1,387 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc (unknown) +// source: private/pbdemo/v1/demo.proto + +// This package contains fake resource types, which are useful for working on +// Consul's generic storage APIs. + +package demov1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Genre int32 + +const ( + Genre_GENRE_UNSPECIFIED Genre = 0 + Genre_GENRE_JAZZ Genre = 1 + Genre_GENRE_FOLK Genre = 2 + Genre_GENRE_POP Genre = 3 + Genre_GENRE_METAL Genre = 4 + Genre_GENRE_PUNK Genre = 5 + Genre_GENRE_BLUES Genre = 6 + Genre_GENRE_R_AND_B Genre = 7 + Genre_GENRE_COUNTRY Genre = 8 + Genre_GENRE_DISCO Genre = 9 + Genre_GENRE_SKA Genre = 10 + Genre_GENRE_HIP_HOP Genre = 11 + Genre_GENRE_INDIE Genre = 12 +) + +// Enum value maps for Genre. +var ( + Genre_name = map[int32]string{ + 0: "GENRE_UNSPECIFIED", + 1: "GENRE_JAZZ", + 2: "GENRE_FOLK", + 3: "GENRE_POP", + 4: "GENRE_METAL", + 5: "GENRE_PUNK", + 6: "GENRE_BLUES", + 7: "GENRE_R_AND_B", + 8: "GENRE_COUNTRY", + 9: "GENRE_DISCO", + 10: "GENRE_SKA", + 11: "GENRE_HIP_HOP", + 12: "GENRE_INDIE", + } + Genre_value = map[string]int32{ + "GENRE_UNSPECIFIED": 0, + "GENRE_JAZZ": 1, + "GENRE_FOLK": 2, + "GENRE_POP": 3, + "GENRE_METAL": 4, + "GENRE_PUNK": 5, + "GENRE_BLUES": 6, + "GENRE_R_AND_B": 7, + "GENRE_COUNTRY": 8, + "GENRE_DISCO": 9, + "GENRE_SKA": 10, + "GENRE_HIP_HOP": 11, + "GENRE_INDIE": 12, + } +) + +func (x Genre) Enum() *Genre { + p := new(Genre) + *p = x + return p +} + +func (x Genre) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Genre) Descriptor() protoreflect.EnumDescriptor { + return file_private_pbdemo_v1_demo_proto_enumTypes[0].Descriptor() +} + +func (Genre) Type() protoreflect.EnumType { + return &file_private_pbdemo_v1_demo_proto_enumTypes[0] +} + +func (x Genre) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Genre.Descriptor instead. +func (Genre) EnumDescriptor() ([]byte, []int) { + return file_private_pbdemo_v1_demo_proto_rawDescGZIP(), []int{0} +} + +type Artist struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + Genre Genre `protobuf:"varint,3,opt,name=genre,proto3,enum=hashicorp.consul.internal.demo.v1.Genre" json:"genre,omitempty"` + GroupMembers int32 `protobuf:"varint,4,opt,name=group_members,json=groupMembers,proto3" json:"group_members,omitempty"` +} + +func (x *Artist) Reset() { + *x = Artist{} + if protoimpl.UnsafeEnabled { + mi := &file_private_pbdemo_v1_demo_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Artist) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Artist) ProtoMessage() {} + +func (x *Artist) ProtoReflect() protoreflect.Message { + mi := &file_private_pbdemo_v1_demo_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Artist.ProtoReflect.Descriptor instead. +func (*Artist) Descriptor() ([]byte, []int) { + return file_private_pbdemo_v1_demo_proto_rawDescGZIP(), []int{0} +} + +func (x *Artist) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Artist) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Artist) GetGenre() Genre { + if x != nil { + return x.Genre + } + return Genre_GENRE_UNSPECIFIED +} + +func (x *Artist) GetGroupMembers() int32 { + if x != nil { + return x.GroupMembers + } + return 0 +} + +type Album struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + YearOfRelease int32 `protobuf:"varint,2,opt,name=year_of_release,json=yearOfRelease,proto3" json:"year_of_release,omitempty"` + CriticallyAclaimed bool `protobuf:"varint,3,opt,name=critically_aclaimed,json=criticallyAclaimed,proto3" json:"critically_aclaimed,omitempty"` + Tracks []string `protobuf:"bytes,4,rep,name=tracks,proto3" json:"tracks,omitempty"` +} + +func (x *Album) Reset() { + *x = Album{} + if protoimpl.UnsafeEnabled { + mi := &file_private_pbdemo_v1_demo_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Album) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Album) ProtoMessage() {} + +func (x *Album) ProtoReflect() protoreflect.Message { + mi := &file_private_pbdemo_v1_demo_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Album.ProtoReflect.Descriptor instead. +func (*Album) Descriptor() ([]byte, []int) { + return file_private_pbdemo_v1_demo_proto_rawDescGZIP(), []int{1} +} + +func (x *Album) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Album) GetYearOfRelease() int32 { + if x != nil { + return x.YearOfRelease + } + return 0 +} + +func (x *Album) GetCriticallyAclaimed() bool { + if x != nil { + return x.CriticallyAclaimed + } + return false +} + +func (x *Album) GetTracks() []string { + if x != nil { + return x.Tracks + } + return nil +} + +var File_private_pbdemo_v1_demo_proto protoreflect.FileDescriptor + +var file_private_pbdemo_v1_demo_proto_rawDesc = []byte{ + 0x0a, 0x1c, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x70, 0x62, 0x64, 0x65, 0x6d, 0x6f, + 0x2f, 0x76, 0x31, 0x2f, 0x64, 0x65, 0x6d, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x21, + 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, + 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x64, 0x65, 0x6d, 0x6f, 0x2e, 0x76, + 0x31, 0x22, 0xa3, 0x01, 0x0a, 0x06, 0x41, 0x72, 0x74, 0x69, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x3e, 0x0a, 0x05, 0x67, 0x65, 0x6e, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x28, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, + 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x64, 0x65, + 0x6d, 0x6f, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x6e, 0x72, 0x65, 0x52, 0x05, 0x67, 0x65, 0x6e, + 0x72, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x6d, 0x65, 0x6d, 0x62, + 0x65, 0x72, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x67, 0x72, 0x6f, 0x75, 0x70, + 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x22, 0x8c, 0x01, 0x0a, 0x05, 0x41, 0x6c, 0x62, 0x75, + 0x6d, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x79, 0x65, 0x61, 0x72, 0x5f, 0x6f, 0x66, + 0x5f, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, + 0x79, 0x65, 0x61, 0x72, 0x4f, 0x66, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x12, 0x2f, 0x0a, + 0x13, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x6c, 0x79, 0x5f, 0x61, 0x63, 0x6c, 0x61, + 0x69, 0x6d, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x63, 0x72, 0x69, 0x74, + 0x69, 0x63, 0x61, 0x6c, 0x6c, 0x79, 0x41, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x65, 0x64, 0x12, 0x16, + 0x0a, 0x06, 0x74, 0x72, 0x61, 0x63, 0x6b, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, + 0x74, 0x72, 0x61, 0x63, 0x6b, 0x73, 0x2a, 0xe9, 0x01, 0x0a, 0x05, 0x47, 0x65, 0x6e, 0x72, 0x65, + 0x12, 0x15, 0x0a, 0x11, 0x47, 0x45, 0x4e, 0x52, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, + 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0e, 0x0a, 0x0a, 0x47, 0x45, 0x4e, 0x52, 0x45, + 0x5f, 0x4a, 0x41, 0x5a, 0x5a, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x47, 0x45, 0x4e, 0x52, 0x45, + 0x5f, 0x46, 0x4f, 0x4c, 0x4b, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x47, 0x45, 0x4e, 0x52, 0x45, + 0x5f, 0x50, 0x4f, 0x50, 0x10, 0x03, 0x12, 0x0f, 0x0a, 0x0b, 0x47, 0x45, 0x4e, 0x52, 0x45, 0x5f, + 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x10, 0x04, 0x12, 0x0e, 0x0a, 0x0a, 0x47, 0x45, 0x4e, 0x52, 0x45, + 0x5f, 0x50, 0x55, 0x4e, 0x4b, 0x10, 0x05, 0x12, 0x0f, 0x0a, 0x0b, 0x47, 0x45, 0x4e, 0x52, 0x45, + 0x5f, 0x42, 0x4c, 0x55, 0x45, 0x53, 0x10, 0x06, 0x12, 0x11, 0x0a, 0x0d, 0x47, 0x45, 0x4e, 0x52, + 0x45, 0x5f, 0x52, 0x5f, 0x41, 0x4e, 0x44, 0x5f, 0x42, 0x10, 0x07, 0x12, 0x11, 0x0a, 0x0d, 0x47, + 0x45, 0x4e, 0x52, 0x45, 0x5f, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x52, 0x59, 0x10, 0x08, 0x12, 0x0f, + 0x0a, 0x0b, 0x47, 0x45, 0x4e, 0x52, 0x45, 0x5f, 0x44, 0x49, 0x53, 0x43, 0x4f, 0x10, 0x09, 0x12, + 0x0d, 0x0a, 0x09, 0x47, 0x45, 0x4e, 0x52, 0x45, 0x5f, 0x53, 0x4b, 0x41, 0x10, 0x0a, 0x12, 0x11, + 0x0a, 0x0d, 0x47, 0x45, 0x4e, 0x52, 0x45, 0x5f, 0x48, 0x49, 0x50, 0x5f, 0x48, 0x4f, 0x50, 0x10, + 0x0b, 0x12, 0x0f, 0x0a, 0x0b, 0x47, 0x45, 0x4e, 0x52, 0x45, 0x5f, 0x49, 0x4e, 0x44, 0x49, 0x45, + 0x10, 0x0c, 0x42, 0x97, 0x02, 0x0a, 0x25, 0x63, 0x6f, 0x6d, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, + 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x64, 0x65, 0x6d, 0x6f, 0x2e, 0x76, 0x31, 0x42, 0x09, 0x44, 0x65, + 0x6d, 0x6f, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, + 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x2f, 0x70, 0x62, 0x64, 0x65, 0x6d, 0x6f, 0x2f, 0x76, 0x31, 0x3b, 0x64, + 0x65, 0x6d, 0x6f, 0x76, 0x31, 0xa2, 0x02, 0x04, 0x48, 0x43, 0x49, 0x44, 0xaa, 0x02, 0x21, 0x48, + 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, + 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x44, 0x65, 0x6d, 0x6f, 0x2e, 0x56, 0x31, + 0xca, 0x02, 0x21, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, + 0x73, 0x75, 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5c, 0x44, 0x65, 0x6d, + 0x6f, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x2d, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x5c, 0x44, 0x65, 0x6d, 0x6f, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x25, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, + 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x3a, 0x3a, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x3a, 0x3a, 0x44, 0x65, 0x6d, 0x6f, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_private_pbdemo_v1_demo_proto_rawDescOnce sync.Once + file_private_pbdemo_v1_demo_proto_rawDescData = file_private_pbdemo_v1_demo_proto_rawDesc +) + +func file_private_pbdemo_v1_demo_proto_rawDescGZIP() []byte { + file_private_pbdemo_v1_demo_proto_rawDescOnce.Do(func() { + file_private_pbdemo_v1_demo_proto_rawDescData = protoimpl.X.CompressGZIP(file_private_pbdemo_v1_demo_proto_rawDescData) + }) + return file_private_pbdemo_v1_demo_proto_rawDescData +} + +var file_private_pbdemo_v1_demo_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_private_pbdemo_v1_demo_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_private_pbdemo_v1_demo_proto_goTypes = []interface{}{ + (Genre)(0), // 0: hashicorp.consul.internal.demo.v1.Genre + (*Artist)(nil), // 1: hashicorp.consul.internal.demo.v1.Artist + (*Album)(nil), // 2: hashicorp.consul.internal.demo.v1.Album +} +var file_private_pbdemo_v1_demo_proto_depIdxs = []int32{ + 0, // 0: hashicorp.consul.internal.demo.v1.Artist.genre:type_name -> hashicorp.consul.internal.demo.v1.Genre + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_private_pbdemo_v1_demo_proto_init() } +func file_private_pbdemo_v1_demo_proto_init() { + if File_private_pbdemo_v1_demo_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_private_pbdemo_v1_demo_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Artist); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_private_pbdemo_v1_demo_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Album); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_private_pbdemo_v1_demo_proto_rawDesc, + NumEnums: 1, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_private_pbdemo_v1_demo_proto_goTypes, + DependencyIndexes: file_private_pbdemo_v1_demo_proto_depIdxs, + EnumInfos: file_private_pbdemo_v1_demo_proto_enumTypes, + MessageInfos: file_private_pbdemo_v1_demo_proto_msgTypes, + }.Build() + File_private_pbdemo_v1_demo_proto = out.File + file_private_pbdemo_v1_demo_proto_rawDesc = nil + file_private_pbdemo_v1_demo_proto_goTypes = nil + file_private_pbdemo_v1_demo_proto_depIdxs = nil +} diff --git a/proto/private/pbdemo/v1/demo.proto b/proto/private/pbdemo/v1/demo.proto new file mode 100644 index 0000000000..2142b96e0a --- /dev/null +++ b/proto/private/pbdemo/v1/demo.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +// This package contains fake resource types, which are useful for working on +// Consul's generic storage APIs. +package hashicorp.consul.internal.demo.v1; + +message Artist { + string name = 1; + string description = 2; + Genre genre = 3; + int32 group_members = 4; +} + +enum Genre { + GENRE_UNSPECIFIED = 0; + GENRE_JAZZ = 1; + GENRE_FOLK = 2; + GENRE_POP = 3; + GENRE_METAL = 4; + GENRE_PUNK = 5; + GENRE_BLUES = 6; + GENRE_R_AND_B = 7; + GENRE_COUNTRY = 8; + GENRE_DISCO = 9; + GENRE_SKA = 10; + GENRE_HIP_HOP = 11; + GENRE_INDIE = 12; +} + +message Album { + string name = 1; + int32 year_of_release = 2; + bool critically_aclaimed = 3; + repeated string tracks = 4; +} diff --git a/proto/private/pbdemo/v2/demo.pb.binary.go b/proto/private/pbdemo/v2/demo.pb.binary.go new file mode 100644 index 0000000000..084f70d5b7 --- /dev/null +++ b/proto/private/pbdemo/v2/demo.pb.binary.go @@ -0,0 +1,28 @@ +// Code generated by protoc-gen-go-binary. DO NOT EDIT. +// source: private/pbdemo/v2/demo.proto + +package demov2 + +import ( + "google.golang.org/protobuf/proto" +) + +// MarshalBinary implements encoding.BinaryMarshaler +func (msg *Artist) MarshalBinary() ([]byte, error) { + return proto.Marshal(msg) +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler +func (msg *Artist) UnmarshalBinary(b []byte) error { + return proto.Unmarshal(b, msg) +} + +// MarshalBinary implements encoding.BinaryMarshaler +func (msg *Album) MarshalBinary() ([]byte, error) { + return proto.Marshal(msg) +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler +func (msg *Album) UnmarshalBinary(b []byte) error { + return proto.Unmarshal(b, msg) +} diff --git a/proto/private/pbdemo/v2/demo.pb.go b/proto/private/pbdemo/v2/demo.pb.go new file mode 100644 index 0000000000..5563251ef5 --- /dev/null +++ b/proto/private/pbdemo/v2/demo.pb.go @@ -0,0 +1,387 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.1 +// protoc (unknown) +// source: private/pbdemo/v2/demo.proto + +// This package contains fake resource types, which are useful for working on +// Consul's generic storage APIs. + +package demov2 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Genre int32 + +const ( + Genre_GENRE_UNSPECIFIED Genre = 0 + Genre_GENRE_JAZZ Genre = 1 + Genre_GENRE_FOLK Genre = 2 + Genre_GENRE_POP Genre = 3 + Genre_GENRE_METAL Genre = 4 + Genre_GENRE_PUNK Genre = 5 + Genre_GENRE_BLUES Genre = 6 + Genre_GENRE_R_AND_B Genre = 7 + Genre_GENRE_COUNTRY Genre = 8 + Genre_GENRE_DISCO Genre = 9 + Genre_GENRE_SKA Genre = 10 + Genre_GENRE_HIP_HOP Genre = 11 + Genre_GENRE_INDIE Genre = 12 +) + +// Enum value maps for Genre. +var ( + Genre_name = map[int32]string{ + 0: "GENRE_UNSPECIFIED", + 1: "GENRE_JAZZ", + 2: "GENRE_FOLK", + 3: "GENRE_POP", + 4: "GENRE_METAL", + 5: "GENRE_PUNK", + 6: "GENRE_BLUES", + 7: "GENRE_R_AND_B", + 8: "GENRE_COUNTRY", + 9: "GENRE_DISCO", + 10: "GENRE_SKA", + 11: "GENRE_HIP_HOP", + 12: "GENRE_INDIE", + } + Genre_value = map[string]int32{ + "GENRE_UNSPECIFIED": 0, + "GENRE_JAZZ": 1, + "GENRE_FOLK": 2, + "GENRE_POP": 3, + "GENRE_METAL": 4, + "GENRE_PUNK": 5, + "GENRE_BLUES": 6, + "GENRE_R_AND_B": 7, + "GENRE_COUNTRY": 8, + "GENRE_DISCO": 9, + "GENRE_SKA": 10, + "GENRE_HIP_HOP": 11, + "GENRE_INDIE": 12, + } +) + +func (x Genre) Enum() *Genre { + p := new(Genre) + *p = x + return p +} + +func (x Genre) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Genre) Descriptor() protoreflect.EnumDescriptor { + return file_private_pbdemo_v2_demo_proto_enumTypes[0].Descriptor() +} + +func (Genre) Type() protoreflect.EnumType { + return &file_private_pbdemo_v2_demo_proto_enumTypes[0] +} + +func (x Genre) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Genre.Descriptor instead. +func (Genre) EnumDescriptor() ([]byte, []int) { + return file_private_pbdemo_v2_demo_proto_rawDescGZIP(), []int{0} +} + +type Artist struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Genre Genre `protobuf:"varint,2,opt,name=genre,proto3,enum=hashicorp.consul.internal.demo.v2.Genre" json:"genre,omitempty"` + GroupMembers map[string]string `protobuf:"bytes,3,rep,name=group_members,json=groupMembers,proto3" json:"group_members,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *Artist) Reset() { + *x = Artist{} + if protoimpl.UnsafeEnabled { + mi := &file_private_pbdemo_v2_demo_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Artist) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Artist) ProtoMessage() {} + +func (x *Artist) ProtoReflect() protoreflect.Message { + mi := &file_private_pbdemo_v2_demo_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Artist.ProtoReflect.Descriptor instead. +func (*Artist) Descriptor() ([]byte, []int) { + return file_private_pbdemo_v2_demo_proto_rawDescGZIP(), []int{0} +} + +func (x *Artist) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Artist) GetGenre() Genre { + if x != nil { + return x.Genre + } + return Genre_GENRE_UNSPECIFIED +} + +func (x *Artist) GetGroupMembers() map[string]string { + if x != nil { + return x.GroupMembers + } + return nil +} + +type Album struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` + YearOfRelease int32 `protobuf:"varint,2,opt,name=year_of_release,json=yearOfRelease,proto3" json:"year_of_release,omitempty"` + CriticallyAclaimed bool `protobuf:"varint,3,opt,name=critically_aclaimed,json=criticallyAclaimed,proto3" json:"critically_aclaimed,omitempty"` + Tracks []string `protobuf:"bytes,4,rep,name=tracks,proto3" json:"tracks,omitempty"` +} + +func (x *Album) Reset() { + *x = Album{} + if protoimpl.UnsafeEnabled { + mi := &file_private_pbdemo_v2_demo_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Album) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Album) ProtoMessage() {} + +func (x *Album) ProtoReflect() protoreflect.Message { + mi := &file_private_pbdemo_v2_demo_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Album.ProtoReflect.Descriptor instead. +func (*Album) Descriptor() ([]byte, []int) { + return file_private_pbdemo_v2_demo_proto_rawDescGZIP(), []int{1} +} + +func (x *Album) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *Album) GetYearOfRelease() int32 { + if x != nil { + return x.YearOfRelease + } + return 0 +} + +func (x *Album) GetCriticallyAclaimed() bool { + if x != nil { + return x.CriticallyAclaimed + } + return false +} + +func (x *Album) GetTracks() []string { + if x != nil { + return x.Tracks + } + return nil +} + +var File_private_pbdemo_v2_demo_proto protoreflect.FileDescriptor + +var file_private_pbdemo_v2_demo_proto_rawDesc = []byte{ + 0x0a, 0x1c, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x70, 0x62, 0x64, 0x65, 0x6d, 0x6f, + 0x2f, 0x76, 0x32, 0x2f, 0x64, 0x65, 0x6d, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x21, + 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, + 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x64, 0x65, 0x6d, 0x6f, 0x2e, 0x76, + 0x32, 0x22, 0xff, 0x01, 0x0a, 0x06, 0x41, 0x72, 0x74, 0x69, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x12, 0x3e, 0x0a, 0x05, 0x67, 0x65, 0x6e, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x28, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, + 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x64, 0x65, 0x6d, 0x6f, + 0x2e, 0x76, 0x32, 0x2e, 0x47, 0x65, 0x6e, 0x72, 0x65, 0x52, 0x05, 0x67, 0x65, 0x6e, 0x72, 0x65, + 0x12, 0x60, 0x0a, 0x0d, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x6d, 0x65, 0x6d, 0x62, 0x65, 0x72, + 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3b, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, + 0x6f, 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2e, 0x64, 0x65, 0x6d, 0x6f, 0x2e, 0x76, 0x32, 0x2e, 0x41, 0x72, 0x74, 0x69, + 0x73, 0x74, 0x2e, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x4d, 0x65, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x4d, 0x65, 0x6d, 0x62, 0x65, + 0x72, 0x73, 0x1a, 0x3f, 0x0a, 0x11, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x4d, 0x65, 0x6d, 0x62, 0x65, + 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x22, 0x8e, 0x01, 0x0a, 0x05, 0x41, 0x6c, 0x62, 0x75, 0x6d, 0x12, 0x14, 0x0a, + 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, + 0x74, 0x6c, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x79, 0x65, 0x61, 0x72, 0x5f, 0x6f, 0x66, 0x5f, 0x72, + 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0d, 0x79, 0x65, + 0x61, 0x72, 0x4f, 0x66, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x12, 0x2f, 0x0a, 0x13, 0x63, + 0x72, 0x69, 0x74, 0x69, 0x63, 0x61, 0x6c, 0x6c, 0x79, 0x5f, 0x61, 0x63, 0x6c, 0x61, 0x69, 0x6d, + 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x12, 0x63, 0x72, 0x69, 0x74, 0x69, 0x63, + 0x61, 0x6c, 0x6c, 0x79, 0x41, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, + 0x74, 0x72, 0x61, 0x63, 0x6b, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x74, 0x72, + 0x61, 0x63, 0x6b, 0x73, 0x2a, 0xe9, 0x01, 0x0a, 0x05, 0x47, 0x65, 0x6e, 0x72, 0x65, 0x12, 0x15, + 0x0a, 0x11, 0x47, 0x45, 0x4e, 0x52, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, + 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0e, 0x0a, 0x0a, 0x47, 0x45, 0x4e, 0x52, 0x45, 0x5f, 0x4a, + 0x41, 0x5a, 0x5a, 0x10, 0x01, 0x12, 0x0e, 0x0a, 0x0a, 0x47, 0x45, 0x4e, 0x52, 0x45, 0x5f, 0x46, + 0x4f, 0x4c, 0x4b, 0x10, 0x02, 0x12, 0x0d, 0x0a, 0x09, 0x47, 0x45, 0x4e, 0x52, 0x45, 0x5f, 0x50, + 0x4f, 0x50, 0x10, 0x03, 0x12, 0x0f, 0x0a, 0x0b, 0x47, 0x45, 0x4e, 0x52, 0x45, 0x5f, 0x4d, 0x45, + 0x54, 0x41, 0x4c, 0x10, 0x04, 0x12, 0x0e, 0x0a, 0x0a, 0x47, 0x45, 0x4e, 0x52, 0x45, 0x5f, 0x50, + 0x55, 0x4e, 0x4b, 0x10, 0x05, 0x12, 0x0f, 0x0a, 0x0b, 0x47, 0x45, 0x4e, 0x52, 0x45, 0x5f, 0x42, + 0x4c, 0x55, 0x45, 0x53, 0x10, 0x06, 0x12, 0x11, 0x0a, 0x0d, 0x47, 0x45, 0x4e, 0x52, 0x45, 0x5f, + 0x52, 0x5f, 0x41, 0x4e, 0x44, 0x5f, 0x42, 0x10, 0x07, 0x12, 0x11, 0x0a, 0x0d, 0x47, 0x45, 0x4e, + 0x52, 0x45, 0x5f, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x52, 0x59, 0x10, 0x08, 0x12, 0x0f, 0x0a, 0x0b, + 0x47, 0x45, 0x4e, 0x52, 0x45, 0x5f, 0x44, 0x49, 0x53, 0x43, 0x4f, 0x10, 0x09, 0x12, 0x0d, 0x0a, + 0x09, 0x47, 0x45, 0x4e, 0x52, 0x45, 0x5f, 0x53, 0x4b, 0x41, 0x10, 0x0a, 0x12, 0x11, 0x0a, 0x0d, + 0x47, 0x45, 0x4e, 0x52, 0x45, 0x5f, 0x48, 0x49, 0x50, 0x5f, 0x48, 0x4f, 0x50, 0x10, 0x0b, 0x12, + 0x0f, 0x0a, 0x0b, 0x47, 0x45, 0x4e, 0x52, 0x45, 0x5f, 0x49, 0x4e, 0x44, 0x49, 0x45, 0x10, 0x0c, + 0x42, 0x97, 0x02, 0x0a, 0x25, 0x63, 0x6f, 0x6d, 0x2e, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, + 0x72, 0x70, 0x2e, 0x63, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x2e, 0x64, 0x65, 0x6d, 0x6f, 0x2e, 0x76, 0x32, 0x42, 0x09, 0x44, 0x65, 0x6d, 0x6f, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x63, 0x6f, + 0x6e, 0x73, 0x75, 0x6c, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x2f, 0x70, 0x62, 0x64, 0x65, 0x6d, 0x6f, 0x2f, 0x76, 0x32, 0x3b, 0x64, 0x65, 0x6d, + 0x6f, 0x76, 0x32, 0xa2, 0x02, 0x04, 0x48, 0x43, 0x49, 0x44, 0xaa, 0x02, 0x21, 0x48, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2e, 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x2e, 0x49, 0x6e, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2e, 0x44, 0x65, 0x6d, 0x6f, 0x2e, 0x56, 0x32, 0xca, 0x02, + 0x21, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, 0x6f, 0x6e, 0x73, 0x75, + 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5c, 0x44, 0x65, 0x6d, 0x6f, 0x5c, + 0x56, 0x32, 0xe2, 0x02, 0x2d, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x5c, 0x43, + 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x5c, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5c, 0x44, + 0x65, 0x6d, 0x6f, 0x5c, 0x56, 0x32, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0xea, 0x02, 0x25, 0x48, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x3a, 0x3a, + 0x43, 0x6f, 0x6e, 0x73, 0x75, 0x6c, 0x3a, 0x3a, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x3a, 0x3a, 0x44, 0x65, 0x6d, 0x6f, 0x3a, 0x3a, 0x56, 0x32, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, +} + +var ( + file_private_pbdemo_v2_demo_proto_rawDescOnce sync.Once + file_private_pbdemo_v2_demo_proto_rawDescData = file_private_pbdemo_v2_demo_proto_rawDesc +) + +func file_private_pbdemo_v2_demo_proto_rawDescGZIP() []byte { + file_private_pbdemo_v2_demo_proto_rawDescOnce.Do(func() { + file_private_pbdemo_v2_demo_proto_rawDescData = protoimpl.X.CompressGZIP(file_private_pbdemo_v2_demo_proto_rawDescData) + }) + return file_private_pbdemo_v2_demo_proto_rawDescData +} + +var file_private_pbdemo_v2_demo_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_private_pbdemo_v2_demo_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_private_pbdemo_v2_demo_proto_goTypes = []interface{}{ + (Genre)(0), // 0: hashicorp.consul.internal.demo.v2.Genre + (*Artist)(nil), // 1: hashicorp.consul.internal.demo.v2.Artist + (*Album)(nil), // 2: hashicorp.consul.internal.demo.v2.Album + nil, // 3: hashicorp.consul.internal.demo.v2.Artist.GroupMembersEntry +} +var file_private_pbdemo_v2_demo_proto_depIdxs = []int32{ + 0, // 0: hashicorp.consul.internal.demo.v2.Artist.genre:type_name -> hashicorp.consul.internal.demo.v2.Genre + 3, // 1: hashicorp.consul.internal.demo.v2.Artist.group_members:type_name -> hashicorp.consul.internal.demo.v2.Artist.GroupMembersEntry + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_private_pbdemo_v2_demo_proto_init() } +func file_private_pbdemo_v2_demo_proto_init() { + if File_private_pbdemo_v2_demo_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_private_pbdemo_v2_demo_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Artist); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_private_pbdemo_v2_demo_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Album); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_private_pbdemo_v2_demo_proto_rawDesc, + NumEnums: 1, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_private_pbdemo_v2_demo_proto_goTypes, + DependencyIndexes: file_private_pbdemo_v2_demo_proto_depIdxs, + EnumInfos: file_private_pbdemo_v2_demo_proto_enumTypes, + MessageInfos: file_private_pbdemo_v2_demo_proto_msgTypes, + }.Build() + File_private_pbdemo_v2_demo_proto = out.File + file_private_pbdemo_v2_demo_proto_rawDesc = nil + file_private_pbdemo_v2_demo_proto_goTypes = nil + file_private_pbdemo_v2_demo_proto_depIdxs = nil +} diff --git a/proto/private/pbdemo/v2/demo.proto b/proto/private/pbdemo/v2/demo.proto new file mode 100644 index 0000000000..f5e47f2452 --- /dev/null +++ b/proto/private/pbdemo/v2/demo.proto @@ -0,0 +1,34 @@ +syntax = "proto3"; + +// This package contains fake resource types, which are useful for working on +// Consul's generic storage APIs. +package hashicorp.consul.internal.demo.v2; + +message Artist { + string name = 1; + Genre genre = 2; + map group_members = 3; +} + +enum Genre { + GENRE_UNSPECIFIED = 0; + GENRE_JAZZ = 1; + GENRE_FOLK = 2; + GENRE_POP = 3; + GENRE_METAL = 4; + GENRE_PUNK = 5; + GENRE_BLUES = 6; + GENRE_R_AND_B = 7; + GENRE_COUNTRY = 8; + GENRE_DISCO = 9; + GENRE_SKA = 10; + GENRE_HIP_HOP = 11; + GENRE_INDIE = 12; +} + +message Album { + string title = 1; + int32 year_of_release = 2; + bool critically_aclaimed = 3; + repeated string tracks = 4; +}