diff --git a/globals/fields.go b/globals/fields.go index 4fd02ee4d..491b00c8d 100644 --- a/globals/fields.go +++ b/globals/fields.go @@ -81,4 +81,5 @@ const ( ConfigurationField = "configuration" WorkerAuthTokenField = "worker_auth_token" WorkerConfigField = "worker_config" + ActiveConnectionCountField = "active_session_count" ) diff --git a/internal/daemon/controller/handlers/workers/worker_service.go b/internal/daemon/controller/handlers/workers/worker_service.go index bb82d654b..98f054bab 100644 --- a/internal/daemon/controller/handlers/workers/worker_service.go +++ b/internal/daemon/controller/handlers/workers/worker_service.go @@ -462,6 +462,9 @@ func toProto(ctx context.Context, in *servers.Worker, opt ...handlers.Option) (* if outputFields.Has(globals.LastStatusTimeField) { out.LastStatusTime = in.GetLastStatusTime().GetTimestamp() } + if outputFields.Has(globals.ActiveConnectionCountField) { + out.ActiveConnectionCount = in.ActiveConnectionCount() + } if outputFields.Has(globals.CanonicalTagsField) && len(in.CanonicalTags()) > 0 { var err error out.CanonicalTags, err = tagsToMapProto(in.CanonicalTags()) diff --git a/internal/db/schema/migrations/oss/postgres/99/02_worker_controller_tables.up.sql b/internal/db/schema/migrations/oss/postgres/99/02_worker_controller_tables.up.sql index a9a70096d..1e8088baa 100644 --- a/internal/db/schema/migrations/oss/postgres/99/02_worker_controller_tables.up.sql +++ b/internal/db/schema/migrations/oss/postgres/99/02_worker_controller_tables.up.sql @@ -150,41 +150,6 @@ create table server_worker_tag ( primary key(worker_id, key, value, source) ); --- worker_aggregate view allows the worker and configuration to be read at the --- same time. -create view server_worker_aggregate as - with worker_config_tags(worker_id, source, tags) as ( - select - ct.worker_id, - ct.source, - -- keys and tags can be any lowercase printable character so use uppercase characters as delimitors. - string_agg(distinct concat_ws('Y', ct.key, ct.value), 'Z') as tags - from server_worker_tag ct - group by ct.worker_id, ct.source - ) -select - w.public_id, - w.scope_id, - w.description, - w.name, - w.address, - w.create_time, - w.update_time, - w.version, - w.worker_reported_name, - w.worker_reported_address, - w.last_status_time, - -- keys and tags can be any lowercase printable character so use uppercase characters as delimitors. - wt.tags as api_tags, - ct.tags as worker_config_tags -from server_worker w - left join worker_config_tags wt on - w.public_id = wt.worker_id and wt.source = 'api' - left join worker_config_tags ct on - w.public_id = ct.worker_id and ct.source = 'configuration'; -comment on view server_worker_aggregate is -'server_worker_aggregate contains the worker resource with its worker provided config values and its configuration and api provided tags.'; - -- Aaand drop server_tag drop table server_tag; @@ -245,26 +210,6 @@ set where status = 'running'; - --- Replaces the view created in 9/01. --- Remove the worker id from this view. In actuality this is almost a no-op --- because no server information was ever getting populated here due to a bug --- in the update mask when updating a session at the time we activate a session. -create view session_list as - select - s.public_id, s.user_id, s.host_id, s.target_id, - s.host_set_id, s.auth_token_id, s.scope_id, s.certificate,s.expiration_time, - s.connection_limit, s.tofu_token, s.key_id, s.termination_reason, s.version, - s.create_time, s.update_time, s.endpoint, s.worker_filter, - ss.state, ss.previous_end_time, ss.start_time, ss.end_time, sc.public_id as connection_id, - sc.client_tcp_address, sc.client_tcp_port, sc.endpoint_tcp_address, sc.endpoint_tcp_port, - sc.bytes_up, sc.bytes_down, sc.closed_reason - from session s - join session_state ss on - s.public_id = ss.session_id - left join session_connection sc on - s.public_id = sc.session_id; - -- Drop the server and server_type_enm tables drop table server; drop table server_type_enm; diff --git a/internal/db/schema/migrations/oss/postgres/99/04_views.up.sql b/internal/db/schema/migrations/oss/postgres/99/04_views.up.sql new file mode 100644 index 000000000..8ac26e22b --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/99/04_views.up.sql @@ -0,0 +1,69 @@ +begin; + +-- worker_aggregate view allows the worker and configuration to be read at the +-- same time. +create view server_worker_aggregate as +with worker_config_tags(worker_id, source, tags) as ( + select + ct.worker_id, + ct.source, + -- keys and tags can be any lowercase printable character so use uppercase characters as delimitors. + string_agg(distinct concat_ws('Y', ct.key, ct.value), 'Z') as tags + from server_worker_tag ct + group by ct.worker_id, ct.source +), + connection_count (worker_id, count) as ( + select + worker_id, + count(1) as count + from session_connection + where closed_reason is null + group by worker_id + ) +select + w.public_id, + w.scope_id, + w.description, + w.name, + w.address, + w.create_time, + w.update_time, + w.version, + w.worker_reported_name, + w.worker_reported_address, + w.last_status_time, + cc.count as active_connection_count, + -- keys and tags can be any lowercase printable character so use uppercase characters as delimitors. + wt.tags as api_tags, + ct.tags as worker_config_tags +from server_worker w + left join worker_config_tags wt on + w.public_id = wt.worker_id and wt.source = 'api' + left join worker_config_tags ct on + w.public_id = ct.worker_id and ct.source = 'configuration' + left join connection_count as cc on + w.public_id = cc.worker_id; +comment on view server_worker_aggregate is + 'server_worker_aggregate contains the worker resource with its worker provided config values and its configuration and api provided tags.'; + + +-- Replaces the view created in 9/01. +-- Remove the worker id from this view. In actuality this is almost a no-op +-- because no server information was ever getting populated here due to a bug +-- in the update mask when updating a session at the time we activate a session. +create view session_list as +select + s.public_id, s.user_id, s.host_id, s.target_id, + s.host_set_id, s.auth_token_id, s.scope_id, s.certificate,s.expiration_time, + s.connection_limit, s.tofu_token, s.key_id, s.termination_reason, s.version, + s.create_time, s.update_time, s.endpoint, s.worker_filter, + ss.state, ss.previous_end_time, ss.start_time, ss.end_time, sc.public_id as connection_id, + sc.client_tcp_address, sc.client_tcp_port, sc.endpoint_tcp_address, sc.endpoint_tcp_port, + sc.bytes_up, sc.bytes_down, sc.closed_reason +from session s + join session_state ss on + s.public_id = ss.session_id + left join session_connection sc on + s.public_id = sc.session_id; + +commit; \ No newline at end of file diff --git a/internal/gen/controller.swagger.json b/internal/gen/controller.swagger.json index c86653d6e..750f1d0e9 100644 --- a/internal/gen/controller.swagger.json +++ b/internal/gen/controller.swagger.json @@ -5586,6 +5586,12 @@ "type": "string", "title": "worker_auth_token is input only. Supports the worker led node\nenrollment flow where this credentials token is produced by a worker. This\ntoken is a base58 encoded types.FetchNodeCredentialsRequest from\nhttps://github.com/hashicorp/nodeenrollment" }, + "active_connection_count": { + "type": "integer", + "format": "int64", + "description": "Output only. The number of connections that this worker is currently handling.", + "readOnly": true + }, "authorized_actions": { "type": "array", "items": { diff --git a/internal/proto/controller/api/resources/workers/v1/worker.proto b/internal/proto/controller/api/resources/workers/v1/worker.proto index a7c16253e..ce4cb28dd 100644 --- a/internal/proto/controller/api/resources/workers/v1/worker.proto +++ b/internal/proto/controller/api/resources/workers/v1/worker.proto @@ -96,6 +96,9 @@ message Worker { // https://github.com/hashicorp/nodeenrollment google.protobuf.StringValue worker_auth_token = 150 [json_name = "worker_auth_token"]; // @gotags: `class:"public"` + // Output only. The number of connections that this worker is currently handling. + uint32 active_connection_count = 160 [json_name = "active_connection_count"]; // @gotags: `class:"public"` + // Output only. The available actions on this resource for the requester. repeated string authorized_actions = 300 [json_name = "authorized_actions"]; // @gotags: `class:"public"` } diff --git a/internal/servers/repository_worker_test.go b/internal/servers/repository_worker_test.go index 732aebc3f..be0807d12 100644 --- a/internal/servers/repository_worker_test.go +++ b/internal/servers/repository_worker_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/servers" "github.com/hashicorp/boundary/internal/servers/store" + "github.com/hashicorp/boundary/internal/session" "github.com/hashicorp/boundary/internal/types/scope" "github.com/hashicorp/go-dbw" "github.com/hashicorp/go-kms-wrapping/extras/kms/v2/migrations" @@ -25,6 +26,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" ) func TestDeleteWorker(t *testing.T) { @@ -162,10 +164,43 @@ func TestLookupWorker(t *testing.T) { servers.WithPublicId(w.GetPublicId())) require.NoError(t, err) + sessRepo, err := session.NewRepository(rw, rw, kms) + require.NoError(t, err) + connRepo, err := session.NewConnectionRepository(ctx, rw, rw, kms, session.WithWorkerStateDelay(0)) + require.NoError(t, err) + // 1 session with 2 connections + { + composedOf := session.TestSessionParams(t, conn, wrapper, iam.TestRepo(t, conn, wrapper)) + future := timestamppb.New(time.Now().Add(time.Hour)) + exp := ×tamp.Timestamp{Timestamp: future} + composedOf.ConnectionLimit = -1 + sess := session.TestSession(t, conn, wrapper, composedOf, session.WithDbOpts(db.WithSkipVetForWrite(true)), session.WithExpirationTime(exp)) + sess, _, err = sessRepo.ActivateSession(ctx, sess.GetPublicId(), sess.Version, []byte("foo")) + require.NoError(t, err) + c, _, err := connRepo.AuthorizeConnection(ctx, sess.GetPublicId(), w.GetPublicId()) + require.NoError(t, err) + require.NotNil(t, c) + c, _, err = connRepo.AuthorizeConnection(ctx, sess.GetPublicId(), w.GetPublicId()) + require.NoError(t, err) + require.NotNil(t, c) + } + + // 1 session with 1 connection + { + sess2 := session.TestDefaultSession(t, conn, wrapper, iam.TestRepo(t, conn, wrapper), + session.WithDbOpts(db.WithSkipVetForWrite(true))) + sess2, _, err = sessRepo.ActivateSession(ctx, sess2.GetPublicId(), sess2.Version, []byte("foo")) + require.NoError(t, err) + c, _, err := connRepo.AuthorizeConnection(ctx, sess2.GetPublicId(), w.GetPublicId()) + require.NoError(t, err) + require.NotNil(t, c) + } + t.Run("success", func(t *testing.T) { got, err := repo.LookupWorker(ctx, w.GetPublicId()) require.NoError(t, err) assert.Empty(t, cmp.Diff(w, got, protocmp.Transform())) + assert.Equal(t, uint32(3), got.ActiveConnectionCount()) assert.Equal(t, map[string][]string{"key": {"val"}}, got.GetApiTags()) assert.Equal(t, map[string][]string{"config": {"test"}}, got.GetConfigTags()) assert.Equal(t, map[string][]string{ diff --git a/internal/servers/worker.go b/internal/servers/worker.go index 4d7beabfd..895c73e6a 100644 --- a/internal/servers/worker.go +++ b/internal/servers/worker.go @@ -36,8 +36,9 @@ func AttachWorkerIdToState(ctx context.Context, workerId string) (*structpb.Stru type Worker struct { *store.Worker - apiTags []*Tag `gorm:"-"` - configTags []*Tag `gorm:"-"` + activeConnectionCount uint32 `gorm:"-"` + apiTags []*Tag `gorm:"-"` + configTags []*Tag `gorm:"-"` } // NewWorker returns a new Worker. Valid options are WithName, WithDescription @@ -112,6 +113,12 @@ func (w *Worker) CanonicalAddress() string { return w.GetWorkerReportedAddress() } +// ActiveConnectionCount is the current number of sessions this worker is handling +// according to the controllers. +func (w *Worker) ActiveConnectionCount() uint32 { + return w.activeConnectionCount +} + // CanonicalTags is the deduplicated set of tags contained on both the resource // set over the API as well as the tags reported by the worker itself. func (w *Worker) CanonicalTags() map[string][]string { @@ -129,6 +136,7 @@ func (w *Worker) CanonicalTags() map[string][]string { return tags } +// GetApiTags returns the tags for this worker which has been set by the api. func (w *Worker) GetApiTags() map[string][]string { tags := make(map[string][]string) for _, t := range w.apiTags { @@ -137,6 +145,8 @@ func (w *Worker) GetApiTags() map[string][]string { return tags } +// GetConfigTags returns the tags for this worker which has been set through +// the worker daemon's configuration file. func (w *Worker) GetConfigTags() map[string][]string { tags := make(map[string][]string) for _, t := range w.configTags { @@ -172,6 +182,8 @@ type workerAggregate struct { Address string Version uint32 ApiTags string + ActiveConnectionCount uint32 + // Config Fields WorkerReportedName string WorkerReportedAddress string LastStatusTime *timestamp.Timestamp @@ -194,6 +206,7 @@ func (a *workerAggregate) toWorker(ctx context.Context) (*Worker, error) { WorkerReportedName: a.WorkerReportedName, LastStatusTime: a.LastStatusTime, }, + activeConnectionCount: a.ActiveConnectionCount, } tags, err := tagsFromAggregatedTagString(ctx, a.ApiTags) if err != nil { diff --git a/sdk/pbs/controller/api/resources/workers/worker.pb.go b/sdk/pbs/controller/api/resources/workers/worker.pb.go index 751e63812..570705428 100644 --- a/sdk/pbs/controller/api/resources/workers/worker.pb.go +++ b/sdk/pbs/controller/api/resources/workers/worker.pb.go @@ -137,6 +137,8 @@ type Worker struct { // token is a base58 encoded types.FetchNodeCredentialsRequest from // https://github.com/hashicorp/nodeenrollment WorkerAuthToken *wrapperspb.StringValue `protobuf:"bytes,150,opt,name=worker_auth_token,proto3" json:"worker_auth_token,omitempty"` // @gotags: `class:"public"` + // Output only. The number of connections that this worker is currently handling. + ActiveConnectionCount uint32 `protobuf:"varint,160,opt,name=active_connection_count,proto3" json:"active_connection_count,omitempty"` // @gotags: `class:"public"` // Output only. The available actions on this resource for the requester. AuthorizedActions []string `protobuf:"bytes,300,rep,name=authorized_actions,proto3" json:"authorized_actions,omitempty"` // @gotags: `class:"public"` } @@ -278,6 +280,13 @@ func (x *Worker) GetWorkerAuthToken() *wrapperspb.StringValue { return nil } +func (x *Worker) GetActiveConnectionCount() uint32 { + if x != nil { + return x.ActiveConnectionCount + } + return 0 +} + func (x *Worker) GetAuthorizedActions() []string { if x != nil { return x.AuthorizedActions @@ -319,7 +328,7 @@ var file_controller_api_resources_workers_v1_worker_proto_rawDesc = []byte{ 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xc6, 0x09, 0x0a, 0x06, 0x57, 0x6f, 0x72, 0x6b, + 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x81, 0x0a, 0x0a, 0x06, 0x57, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x12, 0x43, @@ -381,27 +390,31 @@ var file_controller_api_resources_workers_v1_worker_proto_rawDesc = []byte{ 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x96, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x11, 0x77, 0x6f, 0x72, 0x6b, 0x65, - 0x72, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x2f, 0x0a, 0x12, - 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, - 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x53, 0x0a, - 0x09, 0x54, 0x61, 0x67, 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, 0x30, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x1a, 0x5c, 0x0a, 0x12, 0x43, 0x61, 0x6e, 0x6f, 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x54, - 0x61, 0x67, 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, 0x30, 0x0a, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, - 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, - 0x42, 0x50, 0x5a, 0x4e, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, - 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, - 0x79, 0x2f, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, - 0x65, 0x73, 0x2f, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x73, 0x3b, 0x77, 0x6f, 0x72, 0x6b, 0x65, - 0x72, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x39, 0x0a, 0x17, + 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0xa0, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x17, + 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x02, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, + 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x53, 0x0a, 0x09, 0x54, 0x61, 0x67, 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, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x5c, 0x0a, + 0x12, 0x43, 0x61, 0x6e, 0x6f, 0x6e, 0x69, 0x63, 0x61, 0x6c, 0x54, 0x61, 0x67, 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, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x50, 0x5a, 0x4e, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, + 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x73, 0x64, 0x6b, + 0x2f, 0x70, 0x62, 0x73, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, + 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x77, 0x6f, + 0x72, 0x6b, 0x65, 0x72, 0x73, 0x3b, 0x77, 0x6f, 0x72, 0x6b, 0x65, 0x72, 0x73, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var (