Bladeren bron

converted to use GRPC

Signed-off-by: Alex Meijer <ameijer@kubecost.com>
Alex Meijer 2 jaren geleden
bovenliggende
commit
1ea938bb10

+ 0 - 29
core/pkg/model/customcostrequest.go

@@ -1,29 +0,0 @@
-package model
-
-import (
-	"time"
-
-	"github.com/opencost/opencost/core/pkg/opencost"
-)
-
-type CustomCostRequest struct {
-	// specifies the window for data to be
-	// retrieved
-	TargetWindow *opencost.Window
-
-	// the step size to return
-	Resolution time.Duration
-}
-
-func (c CustomCostRequest) GetTargetWindow() *opencost.Window {
-	return c.TargetWindow
-}
-
-func (c CustomCostRequest) GetTargetResolution() time.Duration {
-	return c.Resolution
-}
-
-type CustomCostRequestInterface interface {
-	GetTargetWindow() *opencost.Window
-	GetTargetResolution() time.Duration
-}

+ 0 - 406
core/pkg/model/customcostresponse.go

@@ -1,406 +0,0 @@
-package model
-
-import "github.com/opencost/opencost/core/pkg/opencost"
-
-// see design at https://link.excalidraw.com/l/ABLQ24dkKai/CBEQtjH6Mr
-// for additional details on how these objects work in the context of
-// opencost's plugin system
-
-type CustomCostResponse struct {
-	// provides metadata on the Custom CostResponse
-	// deliberately left unstructured
-	Metadata map[string]string
-	// declared by plugin
-	// eg snowflake == "data management",
-	// datadog == "observability" etc
-	// intended for top level agg
-	Costsource string
-	// the name of the custom cost source
-	// e.g., "datadog"
-	Domain string
-	// the version of the Custom Cost response
-	// is set by the plugin, will vary between
-	// different plugins
-	Version string
-	// FOCUS billing currency
-	Currency string
-	// the window of the returned objects
-	Window opencost.Window
-	// array of CustomCosts
-	Costs []*CustomCost
-	// any errors in processing
-	Errors []error
-}
-
-// designed to provide a superset of the FOCUS spec
-// https://github.com/FinOps-Open-Cost-and-Usage-Spec/FOCUS_Spec/releases/latest/download/spec.pdf
-type CustomCost struct {
-	// provides metadata on the Custom CostResponse
-	// deliberately left unstructured
-	Metadata map[string]string
-	// the region that the resource was incurred
-	// corresponds to 'availability zone' of FOCUS
-	Zone string
-	// FOCUS billed Cost
-	BilledCost float32
-	// FOCUS billing account name
-	AccountName string
-	// FOCUS charge category
-	ChargeCategory string
-	// FOCUS charge description
-	Description string
-	// FOCUS List Cost
-	ListCost float32
-	// FOCUS List Unit Price
-	ListUnitPrice float32
-	// FOCUS Resource Name
-	ResourceName string
-	// FOCUS Resource type
-	// if not set, assumed to be domain
-	ResourceType string
-	// ID of the individual cost. should be globally
-	// unique. Assigned by plugin on read
-	Id string
-	// the provider's ID for the cost, if
-	// available
-	// FOCUS resource ID
-	ProviderId string
-	// the window of the returned specific
-	// custom cost
-	// equivalent to charge period start/end of FOCUS
-	Window *opencost.Window
-	// Returns key/value sets of labels
-	// equivalent to Tags in focus spec
-	Labels map[string]string
-	// FOCUS usage quantity
-	UsageQty float32
-	// FOCUS usage Unit
-	UsageUnit string
-	// Optional struct to implement other focus
-	// spec attributes
-	ExtendedAttributes *ExtendedCustomCostAttributes
-}
-
-// These parts of the FOCUS spec are not expected
-// to be implemented by every plugin
-// however, if these bits of information are available,
-// they should be provided
-type ExtendedCustomCostAttributes struct {
-	// FOCUS billing period start/end
-	BillingPeriod *opencost.Window
-	// FOCUS Billing Account ID
-	AccountID string
-	// FOCUS Charge Frequency
-	ChargeFrequency string
-	// FOCUS Charge Subcategory
-	Subcategory string
-	// FOCUS Commitment Discount Category
-	CommitmentDiscountCategory string
-	// FOCUS Commitment Discount ID
-	CommitmentDiscountID string
-	// FOCUS Commitment Discount Name
-	CommitmentDiscountName string
-	// FOCUS Commitment Discount Type
-	CommitmentDiscountType string
-	// FOCUS Effective Cost
-	EffectiveCost float32
-	// FOCUS Invoice Issuer
-	InvoiceIssuer string
-	// FOCUS Provider
-	// if unset, assumed to be domain
-	Provider string
-	// FOCUS Publisher
-	// if unset, assumed to be domain
-	Publisher string
-	// FOCUS Service Category
-	// if unset, assumed to be cost source
-	ServiceCategory string
-	// FOCUS Service Name
-	// if unset, assumed to be cost source
-	ServiceName string
-	// FOCUS SKU ID
-	SkuID string
-	// FOCUS SKU Price ID
-	SkuPriceID string
-	// FOCUS Sub Account ID
-	SubAccountID string
-	// FOCUS Sub Account Name
-	SubAccountName string
-	// FOCUS Pricing Quantity
-	PricingQuantity float32
-	// FOCUS Pricing Unit
-	PricingUnit string
-	// FOCUS Pricing Category
-	PricingCategory string
-}
-
-func (c *CustomCostResponse) Clone() CustomCostResponse {
-	win := c.GetWindow().Clone()
-	costClones := []*CustomCost{}
-
-	for _, cost := range c.GetCosts() {
-		clone := cost.Clone()
-		costClones = append(costClones, &clone)
-	}
-
-	errClones := []error{}
-	for _, err := range c.GetErrors() {
-		errClones = append(errClones, err)
-	}
-	return CustomCostResponse{
-		Metadata:   cloneMap(c.GetMetadata()),
-		Costsource: c.GetCostSource(),
-		Domain:     c.GetDomain(),
-		Version:    c.GetVersion(),
-		Currency:   c.GetCurrency(),
-		Window:     win,
-		Costs:      costClones,
-		Errors:     errClones,
-	}
-}
-
-func cloneMap(input map[string]string) map[string]string {
-	if input == nil {
-		return nil
-	}
-	result := map[string]string{}
-	for key, val := range input {
-		result[key] = val
-	}
-	return result
-}
-
-func (c *CustomCost) Clone() CustomCost {
-	win := c.GetWindow().Clone()
-	var ext ExtendedCustomCostAttributes
-	if c.GetExtendedAttributes() != nil {
-		ext = c.GetExtendedAttributes().Clone()
-	}
-	return CustomCost{
-		Metadata:           cloneMap(c.GetMetadata()),
-		Zone:               c.GetCostIncurredZone(),
-		BilledCost:         c.GetBilledCost(),
-		AccountName:        c.GetAccountName(),
-		ChargeCategory:     c.GetChargeCategory(),
-		Description:        c.GetDescription(),
-		ListCost:           c.GetListCost(),
-		ListUnitPrice:      c.GetListUnitPrice(),
-		ResourceName:       c.GetResourceName(),
-		ResourceType:       c.GetResourceType(),
-		Id:                 c.GetID(),
-		ProviderId:         c.GetProviderID(),
-		Window:             &win,
-		Labels:             cloneMap(c.GetLabels()),
-		UsageQty:           c.GetUsageQuantity(),
-		UsageUnit:          c.GetUsageUnit(),
-		ExtendedAttributes: &ext,
-	}
-}
-func (e *ExtendedCustomCostAttributes) Clone() ExtendedCustomCostAttributes {
-	win := e.BillingPeriod.Clone()
-	return ExtendedCustomCostAttributes{
-		BillingPeriod:              &win,
-		AccountID:                  e.GetAccountID(),
-		ChargeFrequency:            e.GetChargeFrequency(),
-		Subcategory:                e.GetSubcategory(),
-		CommitmentDiscountCategory: e.GetCommitmentDiscountCategory(),
-		CommitmentDiscountID:       e.GetCommitmentDiscountID(),
-		CommitmentDiscountName:     e.GetCommitmentDiscountName(),
-		CommitmentDiscountType:     e.GetCommitmentDiscountType(),
-		EffectiveCost:              e.GetEffectiveCost(),
-		InvoiceIssuer:              e.GetInvoiceIssuer(),
-		Provider:                   e.GetProvider(),
-		Publisher:                  e.GetPublisher(),
-		ServiceCategory:            e.GetServiceCategory(),
-		ServiceName:                e.GetServiceName(),
-		SkuID:                      e.GetSKUID(),
-		SkuPriceID:                 e.GetSKUPriceID(),
-		SubAccountID:               e.GetSubAccountID(),
-		SubAccountName:             e.GetSubAccountName(),
-		PricingQuantity:            e.GetPricingQuantity(),
-		PricingUnit:                e.GetPricingUnit(),
-		PricingCategory:            e.GetPricingCategory(),
-	}
-}
-
-func (e *ExtendedCustomCostAttributes) GetBillingPeriod() *opencost.Window {
-	return e.BillingPeriod
-}
-
-func (e *ExtendedCustomCostAttributes) GetAccountID() string {
-	return e.AccountID
-}
-
-func (e *ExtendedCustomCostAttributes) GetChargeFrequency() string {
-	return e.ChargeFrequency
-}
-
-func (e *ExtendedCustomCostAttributes) GetSubcategory() string {
-	return e.Subcategory
-}
-
-func (e *ExtendedCustomCostAttributes) GetCommitmentDiscountCategory() string {
-	return e.CommitmentDiscountCategory
-}
-
-func (e *ExtendedCustomCostAttributes) GetCommitmentDiscountID() string {
-	return e.CommitmentDiscountID
-}
-
-func (e *ExtendedCustomCostAttributes) GetCommitmentDiscountName() string {
-	return e.CommitmentDiscountName
-}
-
-func (e *ExtendedCustomCostAttributes) GetCommitmentDiscountType() string {
-	return e.CommitmentDiscountType
-}
-
-func (e *ExtendedCustomCostAttributes) GetEffectiveCost() float32 {
-	return e.EffectiveCost
-}
-
-func (e *ExtendedCustomCostAttributes) GetInvoiceIssuer() string {
-	return e.InvoiceIssuer
-}
-
-func (e *ExtendedCustomCostAttributes) GetProvider() string {
-	return e.Provider
-}
-
-func (e *ExtendedCustomCostAttributes) GetPublisher() string {
-	return e.Publisher
-}
-
-func (e *ExtendedCustomCostAttributes) GetServiceCategory() string {
-	return e.ServiceCategory
-}
-
-func (e *ExtendedCustomCostAttributes) GetServiceName() string {
-	return e.ServiceName
-}
-
-func (e *ExtendedCustomCostAttributes) GetSKUID() string {
-	return e.SkuID
-}
-
-func (e *ExtendedCustomCostAttributes) GetSKUPriceID() string {
-	return e.SkuPriceID
-}
-
-func (e *ExtendedCustomCostAttributes) GetSubAccountID() string {
-	return e.SubAccountID
-}
-
-func (e *ExtendedCustomCostAttributes) GetSubAccountName() string {
-	return e.SubAccountName
-}
-func (e *ExtendedCustomCostAttributes) GetPricingQuantity() float32 {
-	return e.PricingQuantity
-}
-func (e *ExtendedCustomCostAttributes) GetPricingUnit() string {
-	return e.PricingUnit
-}
-
-func (e *ExtendedCustomCostAttributes) GetPricingCategory() string {
-	return e.PricingCategory
-}
-
-func (d *CustomCost) GetMetadata() map[string]string {
-	return d.Metadata
-}
-
-func (d *CustomCost) GetCostIncurredZone() string {
-	return d.Zone
-}
-
-func (d *CustomCost) GetBilledCost() float32 {
-	return d.BilledCost
-}
-
-func (d *CustomCost) GetAccountName() string {
-	return d.AccountName
-}
-
-func (d *CustomCost) GetChargeCategory() string {
-	return d.ChargeCategory
-}
-
-func (d *CustomCost) GetDescription() string {
-	return d.Description
-}
-
-func (d *CustomCost) GetListCost() float32 {
-	return d.ListCost
-}
-
-func (d *CustomCost) GetListUnitPrice() float32 {
-	return d.ListUnitPrice
-}
-
-func (d *CustomCost) GetResourceName() string {
-	return d.ResourceName
-}
-
-func (d *CustomCost) GetID() string {
-	return d.Id
-}
-
-func (d *CustomCost) GetProviderID() string {
-	return d.ProviderId
-}
-
-func (d *CustomCost) GetWindow() *opencost.Window {
-	return d.Window
-}
-
-func (d *CustomCost) GetLabels() map[string]string {
-	return d.Labels
-}
-
-func (d *CustomCost) GetUsageQuantity() float32 {
-	return d.UsageQty
-}
-
-func (d *CustomCost) GetUsageUnit() string {
-	return d.UsageUnit
-}
-
-func (d *CustomCost) GetExtendedAttributes() *ExtendedCustomCostAttributes {
-	return d.ExtendedAttributes
-}
-
-func (d *CustomCost) GetResourceType() string {
-	return d.ResourceType
-}
-
-func (d *CustomCostResponse) GetMetadata() map[string]string {
-	return d.Metadata
-}
-
-func (d *CustomCostResponse) GetCostSource() string {
-	return d.Costsource
-}
-
-func (d *CustomCostResponse) GetDomain() string {
-	return d.Domain
-}
-
-func (d *CustomCostResponse) GetVersion() string {
-	return d.Version
-}
-
-func (d *CustomCostResponse) GetCurrency() string {
-	return d.Currency
-}
-
-func (d *CustomCostResponse) GetWindow() opencost.Window {
-	return d.Window
-}
-
-func (d *CustomCostResponse) GetCosts() []*CustomCost {
-	return d.Costs
-}
-
-func (d *CustomCostResponse) GetErrors() []error {
-	return d.Errors
-}

+ 1049 - 0
core/pkg/model/pb/messages.pb.go

@@ -0,0 +1,1049 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.30.0
+// 	protoc        v3.21.12
+// source: protos/customcost/messages.proto
+
+package pb
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	durationpb "google.golang.org/protobuf/types/known/durationpb"
+	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+	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 CustomCostRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// the window of the returned objects
+	Start *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=start,proto3" json:"start,omitempty"`
+	End   *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=end,proto3" json:"end,omitempty"`
+	// resolution of steps to return
+	Resolution *durationpb.Duration `protobuf:"bytes,3,opt,name=resolution,proto3" json:"resolution,omitempty"`
+}
+
+func (x *CustomCostRequest) Reset() {
+	*x = CustomCostRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_protos_customcost_messages_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *CustomCostRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CustomCostRequest) ProtoMessage() {}
+
+func (x *CustomCostRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_protos_customcost_messages_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 CustomCostRequest.ProtoReflect.Descriptor instead.
+func (*CustomCostRequest) Descriptor() ([]byte, []int) {
+	return file_protos_customcost_messages_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *CustomCostRequest) GetStart() *timestamppb.Timestamp {
+	if x != nil {
+		return x.Start
+	}
+	return nil
+}
+
+func (x *CustomCostRequest) GetEnd() *timestamppb.Timestamp {
+	if x != nil {
+		return x.End
+	}
+	return nil
+}
+
+func (x *CustomCostRequest) GetResolution() *durationpb.Duration {
+	if x != nil {
+		return x.Resolution
+	}
+	return nil
+}
+
+type CustomCostResponseSet struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Resps []*CustomCostResponse `protobuf:"bytes,1,rep,name=resps,proto3" json:"resps,omitempty"`
+}
+
+func (x *CustomCostResponseSet) Reset() {
+	*x = CustomCostResponseSet{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_protos_customcost_messages_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *CustomCostResponseSet) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CustomCostResponseSet) ProtoMessage() {}
+
+func (x *CustomCostResponseSet) ProtoReflect() protoreflect.Message {
+	mi := &file_protos_customcost_messages_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 CustomCostResponseSet.ProtoReflect.Descriptor instead.
+func (*CustomCostResponseSet) Descriptor() ([]byte, []int) {
+	return file_protos_customcost_messages_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *CustomCostResponseSet) GetResps() []*CustomCostResponse {
+	if x != nil {
+		return x.Resps
+	}
+	return nil
+}
+
+type CustomCostResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// provides metadata on the Custom CostResponse
+	// deliberately left unstructured
+	Metadata map[string]string `protobuf:"bytes,1,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	// declared by plugin
+	// eg snowflake == "data management",
+	// datadog == "observability" etc
+	// intended for top level agg
+	CostSource string `protobuf:"bytes,2,opt,name=cost_source,json=costSource,proto3" json:"cost_source,omitempty"`
+	// the name of the custom cost source
+	// e.g., "datadog"
+	Domain string `protobuf:"bytes,3,opt,name=domain,proto3" json:"domain,omitempty"`
+	// the version of the Custom Cost response
+	// is set by the plugin, will vary between
+	// different plugins
+	Version string `protobuf:"bytes,4,opt,name=version,proto3" json:"version,omitempty"`
+	// FOCUS billing currency
+	Currency string `protobuf:"bytes,5,opt,name=currency,proto3" json:"currency,omitempty"`
+	// the window of the returned objects
+	Start *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=start,proto3" json:"start,omitempty"`
+	End   *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=end,proto3" json:"end,omitempty"`
+	// array of CustomCosts
+	Costs []*CustomCost `protobuf:"bytes,8,rep,name=costs,proto3" json:"costs,omitempty"`
+	// any errors in processing
+	Errors []string `protobuf:"bytes,9,rep,name=errors,proto3" json:"errors,omitempty"`
+}
+
+func (x *CustomCostResponse) Reset() {
+	*x = CustomCostResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_protos_customcost_messages_proto_msgTypes[2]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *CustomCostResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CustomCostResponse) ProtoMessage() {}
+
+func (x *CustomCostResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_protos_customcost_messages_proto_msgTypes[2]
+	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 CustomCostResponse.ProtoReflect.Descriptor instead.
+func (*CustomCostResponse) Descriptor() ([]byte, []int) {
+	return file_protos_customcost_messages_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *CustomCostResponse) GetMetadata() map[string]string {
+	if x != nil {
+		return x.Metadata
+	}
+	return nil
+}
+
+func (x *CustomCostResponse) GetCostSource() string {
+	if x != nil {
+		return x.CostSource
+	}
+	return ""
+}
+
+func (x *CustomCostResponse) GetDomain() string {
+	if x != nil {
+		return x.Domain
+	}
+	return ""
+}
+
+func (x *CustomCostResponse) GetVersion() string {
+	if x != nil {
+		return x.Version
+	}
+	return ""
+}
+
+func (x *CustomCostResponse) GetCurrency() string {
+	if x != nil {
+		return x.Currency
+	}
+	return ""
+}
+
+func (x *CustomCostResponse) GetStart() *timestamppb.Timestamp {
+	if x != nil {
+		return x.Start
+	}
+	return nil
+}
+
+func (x *CustomCostResponse) GetEnd() *timestamppb.Timestamp {
+	if x != nil {
+		return x.End
+	}
+	return nil
+}
+
+func (x *CustomCostResponse) GetCosts() []*CustomCost {
+	if x != nil {
+		return x.Costs
+	}
+	return nil
+}
+
+func (x *CustomCostResponse) GetErrors() []string {
+	if x != nil {
+		return x.Errors
+	}
+	return nil
+}
+
+type CustomCost struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// provides metadata on the Custom CostResponse
+	// deliberately left unstructured
+	Metadata map[string]string `protobuf:"bytes,1,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	// the region that the resource was incurred
+	// corresponds to 'availability zone' of FOCUS
+	Zone string `protobuf:"bytes,2,opt,name=zone,proto3" json:"zone,omitempty"`
+	// FOCUS billing account name
+	AccountName string `protobuf:"bytes,3,opt,name=account_name,json=accountName,proto3" json:"account_name,omitempty"`
+	// FOCUS charge category
+	ChargeCategory string `protobuf:"bytes,4,opt,name=charge_category,json=chargeCategory,proto3" json:"charge_category,omitempty"`
+	// FOCUS charge description
+	Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty"`
+	// FOCUS Resource Name
+	ResourceName string `protobuf:"bytes,6,opt,name=resource_name,json=resourceName,proto3" json:"resource_name,omitempty"`
+	// FOCUS Resource type
+	// if not set, assumed to be domain
+	ResourceType string `protobuf:"bytes,7,opt,name=resource_type,json=resourceType,proto3" json:"resource_type,omitempty"`
+	// ID of the individual cost. should be globally
+	// unique. Assigned by plugin on read
+	Id string `protobuf:"bytes,8,opt,name=id,proto3" json:"id,omitempty"`
+	// the provider's ID for the cost, if
+	// available
+	// FOCUS resource ID
+	ProviderId string `protobuf:"bytes,9,opt,name=provider_id,json=providerId,proto3" json:"provider_id,omitempty"`
+	// FOCUS billed Cost
+	BilledCost float32 `protobuf:"fixed32,10,opt,name=billed_cost,json=billedCost,proto3" json:"billed_cost,omitempty"`
+	// FOCUS List Cost
+	ListCost float32 `protobuf:"fixed32,11,opt,name=list_cost,json=listCost,proto3" json:"list_cost,omitempty"`
+	// FOCUS List Unit Price
+	ListUnitPrice float32 `protobuf:"fixed32,12,opt,name=list_unit_price,json=listUnitPrice,proto3" json:"list_unit_price,omitempty"`
+	// FOCUS usage quantity
+	UsageQuantity float32 `protobuf:"fixed32,13,opt,name=usage_quantity,json=usageQuantity,proto3" json:"usage_quantity,omitempty"`
+	// FOCUS usage Unit
+	UsageUnit string `protobuf:"bytes,14,opt,name=usage_unit,json=usageUnit,proto3" json:"usage_unit,omitempty"`
+	// Returns key/value sets of labels
+	// equivalent to Tags in focus spec
+	Labels map[string]string `protobuf:"bytes,15,rep,name=labels,proto3" json:"labels,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+	// Optional struct to implement other focus
+	// spec attributes
+	ExtendedAttributes *CustomCostExtendedAttributes `protobuf:"bytes,16,opt,name=extended_attributes,json=extendedAttributes,proto3,oneof" json:"extended_attributes,omitempty"`
+}
+
+func (x *CustomCost) Reset() {
+	*x = CustomCost{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_protos_customcost_messages_proto_msgTypes[3]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *CustomCost) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CustomCost) ProtoMessage() {}
+
+func (x *CustomCost) ProtoReflect() protoreflect.Message {
+	mi := &file_protos_customcost_messages_proto_msgTypes[3]
+	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 CustomCost.ProtoReflect.Descriptor instead.
+func (*CustomCost) Descriptor() ([]byte, []int) {
+	return file_protos_customcost_messages_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *CustomCost) GetMetadata() map[string]string {
+	if x != nil {
+		return x.Metadata
+	}
+	return nil
+}
+
+func (x *CustomCost) GetZone() string {
+	if x != nil {
+		return x.Zone
+	}
+	return ""
+}
+
+func (x *CustomCost) GetAccountName() string {
+	if x != nil {
+		return x.AccountName
+	}
+	return ""
+}
+
+func (x *CustomCost) GetChargeCategory() string {
+	if x != nil {
+		return x.ChargeCategory
+	}
+	return ""
+}
+
+func (x *CustomCost) GetDescription() string {
+	if x != nil {
+		return x.Description
+	}
+	return ""
+}
+
+func (x *CustomCost) GetResourceName() string {
+	if x != nil {
+		return x.ResourceName
+	}
+	return ""
+}
+
+func (x *CustomCost) GetResourceType() string {
+	if x != nil {
+		return x.ResourceType
+	}
+	return ""
+}
+
+func (x *CustomCost) GetId() string {
+	if x != nil {
+		return x.Id
+	}
+	return ""
+}
+
+func (x *CustomCost) GetProviderId() string {
+	if x != nil {
+		return x.ProviderId
+	}
+	return ""
+}
+
+func (x *CustomCost) GetBilledCost() float32 {
+	if x != nil {
+		return x.BilledCost
+	}
+	return 0
+}
+
+func (x *CustomCost) GetListCost() float32 {
+	if x != nil {
+		return x.ListCost
+	}
+	return 0
+}
+
+func (x *CustomCost) GetListUnitPrice() float32 {
+	if x != nil {
+		return x.ListUnitPrice
+	}
+	return 0
+}
+
+func (x *CustomCost) GetUsageQuantity() float32 {
+	if x != nil {
+		return x.UsageQuantity
+	}
+	return 0
+}
+
+func (x *CustomCost) GetUsageUnit() string {
+	if x != nil {
+		return x.UsageUnit
+	}
+	return ""
+}
+
+func (x *CustomCost) GetLabels() map[string]string {
+	if x != nil {
+		return x.Labels
+	}
+	return nil
+}
+
+func (x *CustomCost) GetExtendedAttributes() *CustomCostExtendedAttributes {
+	if x != nil {
+		return x.ExtendedAttributes
+	}
+	return nil
+}
+
+type CustomCostExtendedAttributes struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// FOCUS billing period start
+	BillingPeriodStart *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=billing_period_start,json=billingPeriodStart,proto3,oneof" json:"billing_period_start,omitempty"`
+	// FOCUS billing period end
+	BillingPeriodEnd *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=billing_period_end,json=billingPeriodEnd,proto3,oneof" json:"billing_period_end,omitempty"`
+	// FOCUS Billing Account ID
+	AccountId *string `protobuf:"bytes,3,opt,name=account_id,json=accountId,proto3,oneof" json:"account_id,omitempty"`
+	// FOCUS Charge Frequency
+	ChargeFrequency *string `protobuf:"bytes,4,opt,name=charge_frequency,json=chargeFrequency,proto3,oneof" json:"charge_frequency,omitempty"`
+	// FOCUS Charge Subcategory
+	Subcategory *string `protobuf:"bytes,5,opt,name=subcategory,proto3,oneof" json:"subcategory,omitempty"`
+	// FOCUS Commitment Discount Category
+	CommitmentDiscountCategory *string `protobuf:"bytes,6,opt,name=commitment_discount_category,json=commitmentDiscountCategory,proto3,oneof" json:"commitment_discount_category,omitempty"`
+	// FOCUS Commitment Discount ID
+	CommitmentDiscountId *string `protobuf:"bytes,7,opt,name=commitment_discount_id,json=commitmentDiscountId,proto3,oneof" json:"commitment_discount_id,omitempty"`
+	// FOCUS Commitment Discount Name
+	CommitmentDiscountName *string `protobuf:"bytes,8,opt,name=commitment_discount_name,json=commitmentDiscountName,proto3,oneof" json:"commitment_discount_name,omitempty"`
+	// FOCUS Commitment Discount Type
+	CommitmentDiscountType *string `protobuf:"bytes,9,opt,name=commitment_discount_type,json=commitmentDiscountType,proto3,oneof" json:"commitment_discount_type,omitempty"`
+	// FOCUS Effective Cost
+	EffectiveCost *float32 `protobuf:"fixed32,10,opt,name=effective_cost,json=effectiveCost,proto3,oneof" json:"effective_cost,omitempty"`
+	// FOCUS Invoice Issuer
+	InvoiceIssuer *string `protobuf:"bytes,11,opt,name=invoice_issuer,json=invoiceIssuer,proto3,oneof" json:"invoice_issuer,omitempty"`
+	// FOCUS Provider
+	// if unset, assumed to be domain
+	Provider *string `protobuf:"bytes,12,opt,name=provider,proto3,oneof" json:"provider,omitempty"`
+	// FOCUS Publisher
+	// if unset, assumed to be domain
+	Publisher *string `protobuf:"bytes,13,opt,name=publisher,proto3,oneof" json:"publisher,omitempty"`
+	// FOCUS Service Category
+	// if unset, assumed to be cost source
+	ServiceCategory *string `protobuf:"bytes,14,opt,name=service_category,json=serviceCategory,proto3,oneof" json:"service_category,omitempty"`
+	// FOCUS Service Name
+	// if unset, assumed to be cost source
+	ServiceName *string `protobuf:"bytes,15,opt,name=service_name,json=serviceName,proto3,oneof" json:"service_name,omitempty"`
+	// FOCUS SKU ID
+	SkuId *string `protobuf:"bytes,16,opt,name=sku_id,json=skuId,proto3,oneof" json:"sku_id,omitempty"`
+	// FOCUS SKU Price ID
+	SkuPriceId *string `protobuf:"bytes,17,opt,name=sku_price_id,json=skuPriceId,proto3,oneof" json:"sku_price_id,omitempty"`
+	// FOCUS Sub Account ID
+	SubAccountId *string `protobuf:"bytes,18,opt,name=sub_account_id,json=subAccountId,proto3,oneof" json:"sub_account_id,omitempty"`
+	// FOCUS Sub Account Name
+	SubAccountName *string `protobuf:"bytes,19,opt,name=sub_account_name,json=subAccountName,proto3,oneof" json:"sub_account_name,omitempty"`
+	// FOCUS Pricing Quantity
+	PricingQuantity *float32 `protobuf:"fixed32,20,opt,name=pricing_quantity,json=pricingQuantity,proto3,oneof" json:"pricing_quantity,omitempty"`
+	// FOCUS Pricing Unit
+	PricingUnit *string `protobuf:"bytes,21,opt,name=pricing_unit,json=pricingUnit,proto3,oneof" json:"pricing_unit,omitempty"`
+	// FOCUS Pricing Category
+	PricingCategory *string `protobuf:"bytes,22,opt,name=pricing_category,json=pricingCategory,proto3,oneof" json:"pricing_category,omitempty"`
+}
+
+func (x *CustomCostExtendedAttributes) Reset() {
+	*x = CustomCostExtendedAttributes{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_protos_customcost_messages_proto_msgTypes[4]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *CustomCostExtendedAttributes) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CustomCostExtendedAttributes) ProtoMessage() {}
+
+func (x *CustomCostExtendedAttributes) ProtoReflect() protoreflect.Message {
+	mi := &file_protos_customcost_messages_proto_msgTypes[4]
+	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 CustomCostExtendedAttributes.ProtoReflect.Descriptor instead.
+func (*CustomCostExtendedAttributes) Descriptor() ([]byte, []int) {
+	return file_protos_customcost_messages_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *CustomCostExtendedAttributes) GetBillingPeriodStart() *timestamppb.Timestamp {
+	if x != nil {
+		return x.BillingPeriodStart
+	}
+	return nil
+}
+
+func (x *CustomCostExtendedAttributes) GetBillingPeriodEnd() *timestamppb.Timestamp {
+	if x != nil {
+		return x.BillingPeriodEnd
+	}
+	return nil
+}
+
+func (x *CustomCostExtendedAttributes) GetAccountId() string {
+	if x != nil && x.AccountId != nil {
+		return *x.AccountId
+	}
+	return ""
+}
+
+func (x *CustomCostExtendedAttributes) GetChargeFrequency() string {
+	if x != nil && x.ChargeFrequency != nil {
+		return *x.ChargeFrequency
+	}
+	return ""
+}
+
+func (x *CustomCostExtendedAttributes) GetSubcategory() string {
+	if x != nil && x.Subcategory != nil {
+		return *x.Subcategory
+	}
+	return ""
+}
+
+func (x *CustomCostExtendedAttributes) GetCommitmentDiscountCategory() string {
+	if x != nil && x.CommitmentDiscountCategory != nil {
+		return *x.CommitmentDiscountCategory
+	}
+	return ""
+}
+
+func (x *CustomCostExtendedAttributes) GetCommitmentDiscountId() string {
+	if x != nil && x.CommitmentDiscountId != nil {
+		return *x.CommitmentDiscountId
+	}
+	return ""
+}
+
+func (x *CustomCostExtendedAttributes) GetCommitmentDiscountName() string {
+	if x != nil && x.CommitmentDiscountName != nil {
+		return *x.CommitmentDiscountName
+	}
+	return ""
+}
+
+func (x *CustomCostExtendedAttributes) GetCommitmentDiscountType() string {
+	if x != nil && x.CommitmentDiscountType != nil {
+		return *x.CommitmentDiscountType
+	}
+	return ""
+}
+
+func (x *CustomCostExtendedAttributes) GetEffectiveCost() float32 {
+	if x != nil && x.EffectiveCost != nil {
+		return *x.EffectiveCost
+	}
+	return 0
+}
+
+func (x *CustomCostExtendedAttributes) GetInvoiceIssuer() string {
+	if x != nil && x.InvoiceIssuer != nil {
+		return *x.InvoiceIssuer
+	}
+	return ""
+}
+
+func (x *CustomCostExtendedAttributes) GetProvider() string {
+	if x != nil && x.Provider != nil {
+		return *x.Provider
+	}
+	return ""
+}
+
+func (x *CustomCostExtendedAttributes) GetPublisher() string {
+	if x != nil && x.Publisher != nil {
+		return *x.Publisher
+	}
+	return ""
+}
+
+func (x *CustomCostExtendedAttributes) GetServiceCategory() string {
+	if x != nil && x.ServiceCategory != nil {
+		return *x.ServiceCategory
+	}
+	return ""
+}
+
+func (x *CustomCostExtendedAttributes) GetServiceName() string {
+	if x != nil && x.ServiceName != nil {
+		return *x.ServiceName
+	}
+	return ""
+}
+
+func (x *CustomCostExtendedAttributes) GetSkuId() string {
+	if x != nil && x.SkuId != nil {
+		return *x.SkuId
+	}
+	return ""
+}
+
+func (x *CustomCostExtendedAttributes) GetSkuPriceId() string {
+	if x != nil && x.SkuPriceId != nil {
+		return *x.SkuPriceId
+	}
+	return ""
+}
+
+func (x *CustomCostExtendedAttributes) GetSubAccountId() string {
+	if x != nil && x.SubAccountId != nil {
+		return *x.SubAccountId
+	}
+	return ""
+}
+
+func (x *CustomCostExtendedAttributes) GetSubAccountName() string {
+	if x != nil && x.SubAccountName != nil {
+		return *x.SubAccountName
+	}
+	return ""
+}
+
+func (x *CustomCostExtendedAttributes) GetPricingQuantity() float32 {
+	if x != nil && x.PricingQuantity != nil {
+		return *x.PricingQuantity
+	}
+	return 0
+}
+
+func (x *CustomCostExtendedAttributes) GetPricingUnit() string {
+	if x != nil && x.PricingUnit != nil {
+		return *x.PricingUnit
+	}
+	return ""
+}
+
+func (x *CustomCostExtendedAttributes) GetPricingCategory() string {
+	if x != nil && x.PricingCategory != nil {
+		return *x.PricingCategory
+	}
+	return ""
+}
+
+var File_protos_customcost_messages_proto protoreflect.FileDescriptor
+
+var file_protos_customcost_messages_proto_rawDesc = []byte{
+	0x0a, 0x20, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x63,
+	0x6f, 0x73, 0x74, 0x2f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x12, 0x13, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x63, 0x6f, 0x73, 0x74, 0x2e, 0x6d,
+	0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
+	0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
+	0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69,
+	0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xae, 0x01, 0x0a, 0x11, 0x43, 0x75, 0x73,
+	0x74, 0x6f, 0x6d, 0x43, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x30,
+	0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e,
+	0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
+	0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74,
+	0x12, 0x2c, 0x0a, 0x03, 0x65, 0x6e, 0x64, 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,
+	0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, 0x39,
+	0x0a, 0x0a, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01,
+	0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x72,
+	0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x56, 0x0a, 0x15, 0x43, 0x75, 0x73,
+	0x74, 0x6f, 0x6d, 0x43, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53,
+	0x65, 0x74, 0x12, 0x3d, 0x0a, 0x05, 0x72, 0x65, 0x73, 0x70, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28,
+	0x0b, 0x32, 0x27, 0x2e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x63, 0x6f, 0x73, 0x74, 0x2e, 0x6d,
+	0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x6f,
+	0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x05, 0x72, 0x65, 0x73, 0x70,
+	0x73, 0x22, 0xc2, 0x03, 0x0a, 0x12, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x6f, 0x73, 0x74,
+	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x51, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61,
+	0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x35, 0x2e, 0x63, 0x75, 0x73,
+	0x74, 0x6f, 0x6d, 0x63, 0x6f, 0x73, 0x74, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73,
+	0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f,
+	0x6e, 0x73, 0x65, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72,
+	0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1f, 0x0a, 0x0b, 0x63,
+	0x6f, 0x73, 0x74, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x0a, 0x63, 0x6f, 0x73, 0x74, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06,
+	0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x6f,
+	0x6d, 0x61, 0x69, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18,
+	0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1a,
+	0x0a, 0x08, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x08, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x12, 0x30, 0x0a, 0x05, 0x73, 0x74,
+	0x61, 0x72, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
+	0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65,
+	0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x2c, 0x0a, 0x03,
+	0x65, 0x6e, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
+	0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65,
+	0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x03, 0x65, 0x6e, 0x64, 0x12, 0x35, 0x0a, 0x05, 0x63, 0x6f,
+	0x73, 0x74, 0x73, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x75, 0x73, 0x74,
+	0x6f, 0x6d, 0x63, 0x6f, 0x73, 0x74, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e,
+	0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x6f, 0x73, 0x74, 0x52, 0x05, 0x63, 0x6f, 0x73, 0x74,
+	0x73, 0x12, 0x16, 0x0a, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28,
+	0x09, 0x52, 0x06, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74,
+	0x61, 0x64, 0x61, 0x74, 0x61, 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, 0xbe, 0x06, 0x0a, 0x0a, 0x43, 0x75, 0x73, 0x74, 0x6f,
+	0x6d, 0x43, 0x6f, 0x73, 0x74, 0x12, 0x49, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
+	0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d,
+	0x63, 0x6f, 0x73, 0x74, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x43, 0x75,
+	0x73, 0x74, 0x6f, 0x6d, 0x43, 0x6f, 0x73, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74,
+	0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
+	0x12, 0x12, 0x0a, 0x04, 0x7a, 0x6f, 0x6e, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
+	0x7a, 0x6f, 0x6e, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f,
+	0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x6f,
+	0x75, 0x6e, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x63, 0x68, 0x61, 0x72, 0x67,
+	0x65, 0x5f, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x0e, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79,
+	0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18,
+	0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69,
+	0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x6e,
+	0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x73, 0x6f, 0x75,
+	0x72, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x6f, 0x75,
+	0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c,
+	0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0e, 0x0a, 0x02,
+	0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1f, 0x0a, 0x0b,
+	0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28,
+	0x09, 0x52, 0x0a, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1f, 0x0a,
+	0x0b, 0x62, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x0a, 0x20, 0x01,
+	0x28, 0x02, 0x52, 0x0a, 0x62, 0x69, 0x6c, 0x6c, 0x65, 0x64, 0x43, 0x6f, 0x73, 0x74, 0x12, 0x1b,
+	0x0a, 0x09, 0x6c, 0x69, 0x73, 0x74, 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28,
+	0x02, 0x52, 0x08, 0x6c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x6c,
+	0x69, 0x73, 0x74, 0x5f, 0x75, 0x6e, 0x69, 0x74, 0x5f, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x0c,
+	0x20, 0x01, 0x28, 0x02, 0x52, 0x0d, 0x6c, 0x69, 0x73, 0x74, 0x55, 0x6e, 0x69, 0x74, 0x50, 0x72,
+	0x69, 0x63, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x75, 0x73, 0x61, 0x67, 0x65, 0x5f, 0x71, 0x75, 0x61,
+	0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x02, 0x52, 0x0d, 0x75, 0x73, 0x61,
+	0x67, 0x65, 0x51, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x75, 0x73,
+	0x61, 0x67, 0x65, 0x5f, 0x75, 0x6e, 0x69, 0x74, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
+	0x75, 0x73, 0x61, 0x67, 0x65, 0x55, 0x6e, 0x69, 0x74, 0x12, 0x43, 0x0a, 0x06, 0x6c, 0x61, 0x62,
+	0x65, 0x6c, 0x73, 0x18, 0x0f, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x63, 0x75, 0x73, 0x74,
+	0x6f, 0x6d, 0x63, 0x6f, 0x73, 0x74, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e,
+	0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x6f, 0x73, 0x74, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c,
+	0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x67,
+	0x0a, 0x13, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69,
+	0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x63, 0x75,
+	0x73, 0x74, 0x6f, 0x6d, 0x63, 0x6f, 0x73, 0x74, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
+	0x73, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x6f, 0x73, 0x74, 0x45, 0x78, 0x74, 0x65,
+	0x6e, 0x64, 0x65, 0x64, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x48, 0x00,
+	0x52, 0x12, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62,
+	0x75, 0x74, 0x65, 0x73, 0x88, 0x01, 0x01, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64,
+	0x61, 0x74, 0x61, 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, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 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, 0x42,
+	0x16, 0x0a, 0x14, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x74,
+	0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x22, 0x94, 0x0c, 0x0a, 0x1c, 0x43, 0x75, 0x73, 0x74,
+	0x6f, 0x6d, 0x43, 0x6f, 0x73, 0x74, 0x45, 0x78, 0x74, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x41, 0x74,
+	0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x51, 0x0a, 0x14, 0x62, 0x69, 0x6c, 0x6c,
+	0x69, 0x6e, 0x67, 0x5f, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74,
+	0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
+	0x6d, 0x70, 0x48, 0x00, 0x52, 0x12, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x50, 0x65, 0x72,
+	0x69, 0x6f, 0x64, 0x53, 0x74, 0x61, 0x72, 0x74, 0x88, 0x01, 0x01, 0x12, 0x4d, 0x0a, 0x12, 0x62,
+	0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x5f, 0x65, 0x6e,
+	0x64, 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, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
+	0x61, 0x6d, 0x70, 0x48, 0x01, 0x52, 0x10, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x50, 0x65,
+	0x72, 0x69, 0x6f, 0x64, 0x45, 0x6e, 0x64, 0x88, 0x01, 0x01, 0x12, 0x22, 0x0a, 0x0a, 0x61, 0x63,
+	0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02,
+	0x52, 0x09, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x2e,
+	0x0a, 0x10, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x5f, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e,
+	0x63, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x03, 0x52, 0x0f, 0x63, 0x68, 0x61, 0x72,
+	0x67, 0x65, 0x46, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x79, 0x88, 0x01, 0x01, 0x12, 0x25,
+	0x0a, 0x0b, 0x73, 0x75, 0x62, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x05, 0x20,
+	0x01, 0x28, 0x09, 0x48, 0x04, 0x52, 0x0b, 0x73, 0x75, 0x62, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f,
+	0x72, 0x79, 0x88, 0x01, 0x01, 0x12, 0x45, 0x0a, 0x1c, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d,
+	0x65, 0x6e, 0x74, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x63, 0x61, 0x74,
+	0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x48, 0x05, 0x52, 0x1a, 0x63,
+	0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e,
+	0x74, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x88, 0x01, 0x01, 0x12, 0x39, 0x0a, 0x16,
+	0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f,
+	0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x06, 0x52, 0x14,
+	0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75,
+	0x6e, 0x74, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x3d, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x6d, 0x69,
+	0x74, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x6e,
+	0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x48, 0x07, 0x52, 0x16, 0x63, 0x6f, 0x6d,
+	0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x4e,
+	0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x3d, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74,
+	0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x74, 0x79,
+	0x70, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x48, 0x08, 0x52, 0x16, 0x63, 0x6f, 0x6d, 0x6d,
+	0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x54, 0x79,
+	0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x2a, 0x0a, 0x0e, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69,
+	0x76, 0x65, 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x02, 0x48, 0x09, 0x52,
+	0x0d, 0x65, 0x66, 0x66, 0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x43, 0x6f, 0x73, 0x74, 0x88, 0x01,
+	0x01, 0x12, 0x2a, 0x0a, 0x0e, 0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x73, 0x73,
+	0x75, 0x65, 0x72, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x48, 0x0a, 0x52, 0x0d, 0x69, 0x6e, 0x76,
+	0x6f, 0x69, 0x63, 0x65, 0x49, 0x73, 0x73, 0x75, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a,
+	0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x48,
+	0x0b, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x21,
+	0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x72, 0x18, 0x0d, 0x20, 0x01, 0x28,
+	0x09, 0x48, 0x0c, 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x72, 0x88, 0x01,
+	0x01, 0x12, 0x2e, 0x0a, 0x10, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x63, 0x61, 0x74,
+	0x65, 0x67, 0x6f, 0x72, 0x79, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x48, 0x0d, 0x52, 0x0f, 0x73,
+	0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x88, 0x01,
+	0x01, 0x12, 0x26, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d,
+	0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x48, 0x0e, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69,
+	0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x1a, 0x0a, 0x06, 0x73, 0x6b, 0x75,
+	0x5f, 0x69, 0x64, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x48, 0x0f, 0x52, 0x05, 0x73, 0x6b, 0x75,
+	0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x25, 0x0a, 0x0c, 0x73, 0x6b, 0x75, 0x5f, 0x70, 0x72, 0x69,
+	0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x48, 0x10, 0x52, 0x0a, 0x73,
+	0x6b, 0x75, 0x50, 0x72, 0x69, 0x63, 0x65, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x29, 0x0a, 0x0e,
+	0x73, 0x75, 0x62, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x12,
+	0x20, 0x01, 0x28, 0x09, 0x48, 0x11, 0x52, 0x0c, 0x73, 0x75, 0x62, 0x41, 0x63, 0x63, 0x6f, 0x75,
+	0x6e, 0x74, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x10, 0x73, 0x75, 0x62, 0x5f, 0x61,
+	0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x13, 0x20, 0x01, 0x28,
+	0x09, 0x48, 0x12, 0x52, 0x0e, 0x73, 0x75, 0x62, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x4e,
+	0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x12, 0x2e, 0x0a, 0x10, 0x70, 0x72, 0x69, 0x63, 0x69, 0x6e,
+	0x67, 0x5f, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x14, 0x20, 0x01, 0x28, 0x02,
+	0x48, 0x13, 0x52, 0x0f, 0x70, 0x72, 0x69, 0x63, 0x69, 0x6e, 0x67, 0x51, 0x75, 0x61, 0x6e, 0x74,
+	0x69, 0x74, 0x79, 0x88, 0x01, 0x01, 0x12, 0x26, 0x0a, 0x0c, 0x70, 0x72, 0x69, 0x63, 0x69, 0x6e,
+	0x67, 0x5f, 0x75, 0x6e, 0x69, 0x74, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, 0x48, 0x14, 0x52, 0x0b,
+	0x70, 0x72, 0x69, 0x63, 0x69, 0x6e, 0x67, 0x55, 0x6e, 0x69, 0x74, 0x88, 0x01, 0x01, 0x12, 0x2e,
+	0x0a, 0x10, 0x70, 0x72, 0x69, 0x63, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f,
+	0x72, 0x79, 0x18, 0x16, 0x20, 0x01, 0x28, 0x09, 0x48, 0x15, 0x52, 0x0f, 0x70, 0x72, 0x69, 0x63,
+	0x69, 0x6e, 0x67, 0x43, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x88, 0x01, 0x01, 0x42, 0x17,
+	0x0a, 0x15, 0x5f, 0x62, 0x69, 0x6c, 0x6c, 0x69, 0x6e, 0x67, 0x5f, 0x70, 0x65, 0x72, 0x69, 0x6f,
+	0x64, 0x5f, 0x73, 0x74, 0x61, 0x72, 0x74, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x62, 0x69, 0x6c, 0x6c,
+	0x69, 0x6e, 0x67, 0x5f, 0x70, 0x65, 0x72, 0x69, 0x6f, 0x64, 0x5f, 0x65, 0x6e, 0x64, 0x42, 0x0d,
+	0x0a, 0x0b, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x42, 0x13, 0x0a,
+	0x11, 0x5f, 0x63, 0x68, 0x61, 0x72, 0x67, 0x65, 0x5f, 0x66, 0x72, 0x65, 0x71, 0x75, 0x65, 0x6e,
+	0x63, 0x79, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x73, 0x75, 0x62, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f,
+	0x72, 0x79, 0x42, 0x1f, 0x0a, 0x1d, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e,
+	0x74, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x63, 0x61, 0x74, 0x65, 0x67,
+	0x6f, 0x72, 0x79, 0x42, 0x19, 0x0a, 0x17, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65,
+	0x6e, 0x74, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x42, 0x1b,
+	0x0a, 0x19, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x64, 0x69,
+	0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x1b, 0x0a, 0x19, 0x5f,
+	0x63, 0x6f, 0x6d, 0x6d, 0x69, 0x74, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f,
+	0x75, 0x6e, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x65, 0x66, 0x66,
+	0x65, 0x63, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x63, 0x6f, 0x73, 0x74, 0x42, 0x11, 0x0a, 0x0f, 0x5f,
+	0x69, 0x6e, 0x76, 0x6f, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x42, 0x0b,
+	0x0a, 0x09, 0x5f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x42, 0x0c, 0x0a, 0x0a, 0x5f,
+	0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x65, 0x72, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x73, 0x65,
+	0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x42, 0x0f,
+	0x0a, 0x0d, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x42,
+	0x09, 0x0a, 0x07, 0x5f, 0x73, 0x6b, 0x75, 0x5f, 0x69, 0x64, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x73,
+	0x6b, 0x75, 0x5f, 0x70, 0x72, 0x69, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x42, 0x11, 0x0a, 0x0f, 0x5f,
+	0x73, 0x75, 0x62, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x42, 0x13,
+	0x0a, 0x11, 0x5f, 0x73, 0x75, 0x62, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x6e,
+	0x61, 0x6d, 0x65, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x70, 0x72, 0x69, 0x63, 0x69, 0x6e, 0x67, 0x5f,
+	0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x70, 0x72, 0x69,
+	0x63, 0x69, 0x6e, 0x67, 0x5f, 0x75, 0x6e, 0x69, 0x74, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x70, 0x72,
+	0x69, 0x63, 0x69, 0x6e, 0x67, 0x5f, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x32, 0x79,
+	0x0a, 0x11, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x6f, 0x73, 0x74, 0x73, 0x53, 0x6f, 0x75,
+	0x72, 0x63, 0x65, 0x12, 0x64, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d,
+	0x43, 0x6f, 0x73, 0x74, 0x73, 0x12, 0x26, 0x2e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x63, 0x6f,
+	0x73, 0x74, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x43, 0x75, 0x73, 0x74,
+	0x6f, 0x6d, 0x43, 0x6f, 0x73, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e,
+	0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x63, 0x6f, 0x73, 0x74, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61,
+	0x67, 0x65, 0x73, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x6f, 0x73, 0x74, 0x52, 0x65,
+	0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x53, 0x65, 0x74, 0x42, 0x30, 0x5a, 0x2e, 0x67, 0x69, 0x74,
+	0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6f, 0x73, 0x74,
+	0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6f, 0x73, 0x74, 0x2f, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x70,
+	0x6b, 0x67, 0x2f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x33,
+}
+
+var (
+	file_protos_customcost_messages_proto_rawDescOnce sync.Once
+	file_protos_customcost_messages_proto_rawDescData = file_protos_customcost_messages_proto_rawDesc
+)
+
+func file_protos_customcost_messages_proto_rawDescGZIP() []byte {
+	file_protos_customcost_messages_proto_rawDescOnce.Do(func() {
+		file_protos_customcost_messages_proto_rawDescData = protoimpl.X.CompressGZIP(file_protos_customcost_messages_proto_rawDescData)
+	})
+	return file_protos_customcost_messages_proto_rawDescData
+}
+
+var file_protos_customcost_messages_proto_msgTypes = make([]protoimpl.MessageInfo, 8)
+var file_protos_customcost_messages_proto_goTypes = []interface{}{
+	(*CustomCostRequest)(nil),            // 0: customcost.messages.CustomCostRequest
+	(*CustomCostResponseSet)(nil),        // 1: customcost.messages.CustomCostResponseSet
+	(*CustomCostResponse)(nil),           // 2: customcost.messages.CustomCostResponse
+	(*CustomCost)(nil),                   // 3: customcost.messages.CustomCost
+	(*CustomCostExtendedAttributes)(nil), // 4: customcost.messages.CustomCostExtendedAttributes
+	nil,                                  // 5: customcost.messages.CustomCostResponse.MetadataEntry
+	nil,                                  // 6: customcost.messages.CustomCost.MetadataEntry
+	nil,                                  // 7: customcost.messages.CustomCost.LabelsEntry
+	(*timestamppb.Timestamp)(nil),        // 8: google.protobuf.Timestamp
+	(*durationpb.Duration)(nil),          // 9: google.protobuf.Duration
+}
+var file_protos_customcost_messages_proto_depIdxs = []int32{
+	8,  // 0: customcost.messages.CustomCostRequest.start:type_name -> google.protobuf.Timestamp
+	8,  // 1: customcost.messages.CustomCostRequest.end:type_name -> google.protobuf.Timestamp
+	9,  // 2: customcost.messages.CustomCostRequest.resolution:type_name -> google.protobuf.Duration
+	2,  // 3: customcost.messages.CustomCostResponseSet.resps:type_name -> customcost.messages.CustomCostResponse
+	5,  // 4: customcost.messages.CustomCostResponse.metadata:type_name -> customcost.messages.CustomCostResponse.MetadataEntry
+	8,  // 5: customcost.messages.CustomCostResponse.start:type_name -> google.protobuf.Timestamp
+	8,  // 6: customcost.messages.CustomCostResponse.end:type_name -> google.protobuf.Timestamp
+	3,  // 7: customcost.messages.CustomCostResponse.costs:type_name -> customcost.messages.CustomCost
+	6,  // 8: customcost.messages.CustomCost.metadata:type_name -> customcost.messages.CustomCost.MetadataEntry
+	7,  // 9: customcost.messages.CustomCost.labels:type_name -> customcost.messages.CustomCost.LabelsEntry
+	4,  // 10: customcost.messages.CustomCost.extended_attributes:type_name -> customcost.messages.CustomCostExtendedAttributes
+	8,  // 11: customcost.messages.CustomCostExtendedAttributes.billing_period_start:type_name -> google.protobuf.Timestamp
+	8,  // 12: customcost.messages.CustomCostExtendedAttributes.billing_period_end:type_name -> google.protobuf.Timestamp
+	0,  // 13: customcost.messages.CustomCostsSource.GetCustomCosts:input_type -> customcost.messages.CustomCostRequest
+	1,  // 14: customcost.messages.CustomCostsSource.GetCustomCosts:output_type -> customcost.messages.CustomCostResponseSet
+	14, // [14:15] is the sub-list for method output_type
+	13, // [13:14] is the sub-list for method input_type
+	13, // [13:13] is the sub-list for extension type_name
+	13, // [13:13] is the sub-list for extension extendee
+	0,  // [0:13] is the sub-list for field type_name
+}
+
+func init() { file_protos_customcost_messages_proto_init() }
+func file_protos_customcost_messages_proto_init() {
+	if File_protos_customcost_messages_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_protos_customcost_messages_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*CustomCostRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_protos_customcost_messages_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*CustomCostResponseSet); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_protos_customcost_messages_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*CustomCostResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_protos_customcost_messages_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*CustomCost); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_protos_customcost_messages_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*CustomCostExtendedAttributes); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	file_protos_customcost_messages_proto_msgTypes[3].OneofWrappers = []interface{}{}
+	file_protos_customcost_messages_proto_msgTypes[4].OneofWrappers = []interface{}{}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_protos_customcost_messages_proto_rawDesc,
+			NumEnums:      0,
+			NumMessages:   8,
+			NumExtensions: 0,
+			NumServices:   1,
+		},
+		GoTypes:           file_protos_customcost_messages_proto_goTypes,
+		DependencyIndexes: file_protos_customcost_messages_proto_depIdxs,
+		MessageInfos:      file_protos_customcost_messages_proto_msgTypes,
+	}.Build()
+	File_protos_customcost_messages_proto = out.File
+	file_protos_customcost_messages_proto_rawDesc = nil
+	file_protos_customcost_messages_proto_goTypes = nil
+	file_protos_customcost_messages_proto_depIdxs = nil
+}

+ 109 - 0
core/pkg/model/pb/messages_grpc.pb.go

@@ -0,0 +1,109 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.3.0
+// - protoc             v3.21.12
+// source: protos/customcost/messages.proto
+
+package pb
+
+import (
+	context "context"
+	grpc "google.golang.org/grpc"
+	codes "google.golang.org/grpc/codes"
+	status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.32.0 or later.
+const _ = grpc.SupportPackageIsVersion7
+
+const (
+	CustomCostsSource_GetCustomCosts_FullMethodName = "/customcost.messages.CustomCostsSource/GetCustomCosts"
+)
+
+// CustomCostsSourceClient is the client API for CustomCostsSource service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+type CustomCostsSourceClient interface {
+	GetCustomCosts(ctx context.Context, in *CustomCostRequest, opts ...grpc.CallOption) (*CustomCostResponseSet, error)
+}
+
+type customCostsSourceClient struct {
+	cc grpc.ClientConnInterface
+}
+
+func NewCustomCostsSourceClient(cc grpc.ClientConnInterface) CustomCostsSourceClient {
+	return &customCostsSourceClient{cc}
+}
+
+func (c *customCostsSourceClient) GetCustomCosts(ctx context.Context, in *CustomCostRequest, opts ...grpc.CallOption) (*CustomCostResponseSet, error) {
+	out := new(CustomCostResponseSet)
+	err := c.cc.Invoke(ctx, CustomCostsSource_GetCustomCosts_FullMethodName, in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+// CustomCostsSourceServer is the server API for CustomCostsSource service.
+// All implementations must embed UnimplementedCustomCostsSourceServer
+// for forward compatibility
+type CustomCostsSourceServer interface {
+	GetCustomCosts(context.Context, *CustomCostRequest) (*CustomCostResponseSet, error)
+	mustEmbedUnimplementedCustomCostsSourceServer()
+}
+
+// UnimplementedCustomCostsSourceServer must be embedded to have forward compatible implementations.
+type UnimplementedCustomCostsSourceServer struct {
+}
+
+func (UnimplementedCustomCostsSourceServer) GetCustomCosts(context.Context, *CustomCostRequest) (*CustomCostResponseSet, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method GetCustomCosts not implemented")
+}
+func (UnimplementedCustomCostsSourceServer) mustEmbedUnimplementedCustomCostsSourceServer() {}
+
+// UnsafeCustomCostsSourceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to CustomCostsSourceServer will
+// result in compilation errors.
+type UnsafeCustomCostsSourceServer interface {
+	mustEmbedUnimplementedCustomCostsSourceServer()
+}
+
+func RegisterCustomCostsSourceServer(s grpc.ServiceRegistrar, srv CustomCostsSourceServer) {
+	s.RegisterService(&CustomCostsSource_ServiceDesc, srv)
+}
+
+func _CustomCostsSource_GetCustomCosts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(CustomCostRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(CustomCostsSourceServer).GetCustomCosts(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: CustomCostsSource_GetCustomCosts_FullMethodName,
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(CustomCostsSourceServer).GetCustomCosts(ctx, req.(*CustomCostRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+// CustomCostsSource_ServiceDesc is the grpc.ServiceDesc for CustomCostsSource service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var CustomCostsSource_ServiceDesc = grpc.ServiceDesc{
+	ServiceName: "customcost.messages.CustomCostsSource",
+	HandlerType: (*CustomCostsSourceServer)(nil),
+	Methods: []grpc.MethodDesc{
+		{
+			MethodName: "GetCustomCosts",
+			Handler:    _CustomCostsSource_GetCustomCosts_Handler,
+		},
+	},
+	Streams:  []grpc.StreamDesc{},
+	Metadata: "protos/customcost/messages.proto",
+}

+ 46 - 0
core/pkg/plugin/grpc.go

@@ -0,0 +1,46 @@
+package plugin
+
+import (
+	"context"
+
+	"github.com/opencost/opencost/core/pkg/model/pb"
+)
+
+// GRPCClient is an implementation of CustomCostsSource that talks over RPC.
+type GRPCClient struct{ client pb.CustomCostsSourceClient }
+
+func (m *GRPCClient) GetCustomCosts(req pb.CustomCostRequest) []pb.CustomCostResponse {
+	resp, err := m.client.GetCustomCosts(context.Background(), &req)
+	if err != nil {
+		return []pb.CustomCostResponse{
+			{
+				Errors: []string{err.Error()},
+			},
+		}
+	}
+	derefs := []pb.CustomCostResponse{}
+	for _, resp := range resp.Resps {
+		derefs = append(derefs, *resp)
+	}
+	return derefs
+}
+
+// Here is the gRPC server that GRPCClient talks to.
+type GRPCServer struct {
+	pb.UnimplementedCustomCostsSourceServer
+	// This is the real implementation
+	Impl CustomCostSource
+}
+
+func (m *GRPCServer) GetCustomCosts(
+	ctx context.Context,
+	req *pb.CustomCostRequest) (*pb.CustomCostResponseSet, error) {
+	ptrs := []*pb.CustomCostResponse{}
+	costs := m.Impl.GetCustomCosts(*req)
+	for _, cost := range costs {
+		ptrs = append(ptrs, &cost)
+	}
+	return &pb.CustomCostResponseSet{
+		Resps: ptrs,
+	}, nil
+}

+ 10 - 44
core/pkg/plugin/plugin_interface.go

@@ -1,55 +1,20 @@
 package plugin
 
 import (
-	"encoding/gob"
-	"fmt"
-	"net/rpc"
+	"context"
 
 	"github.com/hashicorp/go-plugin"
-	"github.com/opencost/opencost/core/pkg/log"
-	"github.com/opencost/opencost/core/pkg/model"
+	"github.com/opencost/opencost/core/pkg/model/pb"
+	grpc "google.golang.org/grpc"
 )
 
 // plugin interface
 type CustomCostSource interface {
-	GetCustomCosts(req model.CustomCostRequestInterface) []model.CustomCostResponse
-}
-
-// RPC impl
-type CustomCostRPC struct{ client *rpc.Client }
-
-func init() {
-	gob.Register(model.CustomCostRequest{})
-}
-func (c *CustomCostRPC) GetCustomCosts(req model.CustomCostRequestInterface) []model.CustomCostResponse {
-
-	var resp []model.CustomCostResponse
-	err := c.client.Call("Plugin.GetCustomCosts", &req, &resp)
-	if err != nil {
-		log.Errorf("error calling plugin: %v", err)
-		resp = []model.CustomCostResponse{
-			{
-				Errors: []error{
-					fmt.Errorf("error calling plugin: %v", err),
-				},
-			},
-		}
-	}
-
-	return resp
-}
-
-type CustomCostRPCServer struct {
-	// This is the real implementation
-	Impl CustomCostSource
-}
-
-func (s *CustomCostRPCServer) GetCustomCosts(args interface{}, resp *[]model.CustomCostResponse) error {
-	*resp = s.Impl.GetCustomCosts(args.(model.CustomCostRequestInterface))
-	return nil
+	GetCustomCosts(req pb.CustomCostRequest) []pb.CustomCostResponse
 }
 
 type CustomCostPlugin struct {
+	plugin.Plugin
 	// Impl Injection
 	Impl CustomCostSource
 }
@@ -57,13 +22,14 @@ type CustomCostPlugin struct {
 // this method is called for as part of the reference plugin implementation
 // see https://github.com/hashicorp/go-plugin/blob/main/examples/basic/shared/greeter_interface.go#L59
 // for context and details
-func (p *CustomCostPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
-	return &CustomCostRPCServer{Impl: p.Impl}, nil
+func (p *CustomCostPlugin) GRPCServer(broker *plugin.GRPCBroker, s *grpc.Server) error {
+	pb.RegisterCustomCostsSourceServer(s, &GRPCServer{Impl: p.Impl})
+	return nil
 }
 
 // this method is called for as part of the reference plugin implementation
 // see https://github.com/hashicorp/go-plugin/blob/main/examples/basic/shared/greeter_interface.go#L63
 // for context and details
-func (CustomCostPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
-	return &CustomCostRPC{client: c}, nil
+func (CustomCostPlugin) GRPCClient(context context.Context, b *plugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) {
+	return &GRPCClient{client: pb.NewCustomCostsSourceClient(c)}, nil
 }

+ 6 - 0
generate.sh

@@ -0,0 +1,6 @@
+#!/bin/zsh
+#
+
+protoc --go_out=./core --go_opt=module=github.com/opencost/opencost/core \
+    --go-grpc_out=./core --go-grpc_opt=module=github.com/opencost/opencost/core \
+    protos/**/*.proto

+ 1 - 1
go.mod

@@ -32,6 +32,7 @@ require (
 	github.com/davecgh/go-spew v1.1.1
 	github.com/getsentry/sentry-go v0.25.0
 	github.com/google/uuid v1.6.0
+	github.com/hashicorp/go-hclog v1.6.2
 	github.com/hashicorp/go-plugin v1.6.0
 	github.com/jszwec/csvutil v1.2.1
 	github.com/julienschmidt/httprouter v1.3.0
@@ -123,7 +124,6 @@ require (
 	github.com/googleapis/gax-go/v2 v2.12.0 // indirect
 	github.com/gorilla/css v1.0.0 // indirect
 	github.com/hashicorp/errwrap v1.0.0 // indirect
-	github.com/hashicorp/go-hclog v1.6.2 // indirect
 	github.com/hashicorp/go-multierror v1.1.1 // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/hashicorp/yamux v0.1.1 // indirect

+ 12 - 9
pkg/customcost/ingestor.go

@@ -8,9 +8,11 @@ import (
 	"time"
 
 	"github.com/hashicorp/go-plugin"
+	"google.golang.org/protobuf/types/known/durationpb"
+	"google.golang.org/protobuf/types/known/timestamppb"
 
 	"github.com/opencost/opencost/core/pkg/log"
-	"github.com/opencost/opencost/core/pkg/model"
+	"github.com/opencost/opencost/core/pkg/model/pb"
 	"github.com/opencost/opencost/core/pkg/opencost"
 	ocplugin "github.com/opencost/opencost/core/pkg/plugin"
 	"github.com/opencost/opencost/core/pkg/util/stringutil"
@@ -157,10 +159,10 @@ func (ing *CustomCostIngestor) BuildWindow(start, end time.Time) {
 }
 
 func (ing *CustomCostIngestor) buildSingleDomain(start, end time.Time, domain string) {
-	target := opencost.NewWindow(&start, &end)
-	req := model.CustomCostRequest{
-		TargetWindow: &target,
-		Resolution:   ing.resolution,
+	req := pb.CustomCostRequest{
+		Start:      timestamppb.New(start),
+		End:        timestamppb.New(end),
+		Resolution: durationpb.New(ing.resolution),
 	}
 	log.Infof("ingestor: building window %s for plugin %s", opencost.NewWindow(&start, &end), domain)
 	// make RPC call via plugin
@@ -177,6 +179,7 @@ func (ing *CustomCostIngestor) buildSingleDomain(start, end time.Time, domain st
 		return
 	}
 
+	// TODO HAVE PLUG IN RETURN DATA AS PROTOBUF
 	// Request the plugin
 	raw, err := rpcClient.Dispense("CustomCostSource")
 	if err != nil {
@@ -195,17 +198,17 @@ func (ing *CustomCostIngestor) buildSingleDomain(start, end time.Time, domain st
 			for _, errResp := range ccr.Errors {
 				log.Errorf("error in getting custom costs for plugin %s: %v", domain, errResp)
 			}
-			log.Errorf("not adding any costs for window %v on plugin %s", req.TargetWindow, domain)
+			log.Errorf("not adding any costs for window %v-%v on plugin %s", req.Start, req.End, domain)
 			continue
 		}
-		log.Debugf("BuildWindow[%s]: GetCustomCost: writing custom costs for window %s: %d", domain, ccr.Window, len(ccr.Costs))
+		log.Debugf("BuildWindow[%s]: GetCustomCost: writing custom costs for window %v-%v: %d", domain, ccr.Start, ccr.End, len(ccr.Costs))
 
 		err2 := ing.repo.Put(&ccr)
 		if err2 != nil {
-			log.Errorf("CustomCost[%s]: ingestor: failed to save Custom Cost Set with window %s: %s", domain, ccr.GetWindow().String(), err2.Error())
+			log.Errorf("CustomCost[%s]: ingestor: failed to save Custom Cost Set with window %v-%v: %s", domain, ccr.Start, ccr.End, err2.Error())
 		}
 
-		ing.expandCoverage(ccr.Window, domain)
+		ing.expandCoverage(opencost.NewClosedWindow(ccr.Start.AsTime(), ccr.End.AsTime()), domain)
 	}
 }
 

+ 21 - 13
pkg/customcost/memoryrepository.go

@@ -5,20 +5,21 @@ import (
 	"sync"
 	"time"
 
-	"github.com/opencost/opencost/core/pkg/model"
+	"github.com/opencost/opencost/core/pkg/model/pb"
 	"golang.org/x/exp/maps"
+	"google.golang.org/protobuf/proto"
 )
 
 // MemoryRepository is an implementation of Repository that uses a map keyed on config key and window start along with a
 // RWMutex to make it threadsafe
 type MemoryRepository struct {
 	rwLock sync.RWMutex
-	data   map[string]map[time.Time]*model.CustomCostResponse
+	data   map[string]map[time.Time][]byte
 }
 
 func NewMemoryRepository() *MemoryRepository {
 	return &MemoryRepository{
-		data: make(map[string]map[time.Time]*model.CustomCostResponse),
+		data: make(map[string]map[time.Time][]byte),
 	}
 }
 
@@ -35,7 +36,7 @@ func (m *MemoryRepository) Has(startTime time.Time, domain string) (bool, error)
 	return ook, nil
 }
 
-func (m *MemoryRepository) Get(startTime time.Time, domain string) (*model.CustomCostResponse, error) {
+func (m *MemoryRepository) Get(startTime time.Time, domain string) (*pb.CustomCostResponse, error) {
 	m.rwLock.RLock()
 	defer m.rwLock.RUnlock()
 
@@ -44,13 +45,17 @@ func (m *MemoryRepository) Get(startTime time.Time, domain string) (*model.Custo
 		return nil, nil
 	}
 
-	cc, ook := domainData[startTime.UTC()]
+	b, ook := domainData[startTime.UTC()]
 	if !ook {
 		return nil, nil
 	}
 
-	clone := cc.Clone()
-	return &clone, nil
+	var ccr *pb.CustomCostResponse
+	err := proto.Unmarshal(b, ccr)
+	if err != nil {
+		return nil, fmt.Errorf("error unmarshalling data: %w")
+	}
+	return ccr, nil
 }
 
 func (m *MemoryRepository) Keys() ([]string, error) {
@@ -61,7 +66,7 @@ func (m *MemoryRepository) Keys() ([]string, error) {
 	return keys, nil
 }
 
-func (m *MemoryRepository) Put(ccr *model.CustomCostResponse) error {
+func (m *MemoryRepository) Put(ccr *pb.CustomCostResponse) error {
 	m.rwLock.Lock()
 	defer m.rwLock.Unlock()
 
@@ -69,8 +74,8 @@ func (m *MemoryRepository) Put(ccr *model.CustomCostResponse) error {
 		return fmt.Errorf("MemoryRepository: Put: cannot save nil")
 	}
 
-	if ccr.Window.IsOpen() {
-		return fmt.Errorf("MemoryRepository: Put: custom cost response has invalid window %s", ccr.Window.String())
+	if ccr.Start == nil || ccr.End == nil {
+		return fmt.Errorf("MemoryRepository: Put: custom cost response has invalid window")
 	}
 
 	if ccr.GetDomain() == "" {
@@ -78,10 +83,13 @@ func (m *MemoryRepository) Put(ccr *model.CustomCostResponse) error {
 	}
 
 	if _, ok := m.data[ccr.GetDomain()]; !ok {
-		m.data[ccr.GetDomain()] = make(map[time.Time]*model.CustomCostResponse)
+		m.data[ccr.GetDomain()] = make(map[time.Time][]byte)
 	}
-
-	m.data[ccr.GetDomain()][ccr.Window.Start().UTC()] = ccr
+	b, err := proto.Marshal(ccr)
+	if err != nil {
+		return fmt.Errorf("MemoryRepository: Put: custom cost could not be marshalled")
+	}
+	m.data[ccr.GetDomain()][ccr.Start.AsTime().UTC()] = b
 
 	return nil
 }

+ 5 - 4
pkg/customcost/pipelineservice.go

@@ -84,10 +84,11 @@ func getRegisteredPlugins(configDir string, execDir string) (map[string]*plugin.
 			"CustomCostSource": &ocplugin.CustomCostPlugin{},
 		}
 		configs[name] = &plugin.ClientConfig{
-			HandshakeConfig: handshakeConfig,
-			Plugins:         pluginMap,
-			Cmd:             exec.Command(fmt.Sprintf(execFmt, execDir, name, runtime.GOOS, version.Architecture), config),
-			Logger:          logger,
+			HandshakeConfig:  handshakeConfig,
+			Plugins:          pluginMap,
+			Cmd:              exec.Command(fmt.Sprintf(execFmt, execDir, name, runtime.GOOS, version.Architecture), config),
+			Logger:           logger,
+			AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
 		}
 	}
 

+ 3 - 3
pkg/customcost/repository.go

@@ -3,14 +3,14 @@ package customcost
 import (
 	"time"
 
-	"github.com/opencost/opencost/core/pkg/model"
+	"github.com/opencost/opencost/core/pkg/model/pb"
 )
 
 // Repository is an interface for storing and retrieving CloudCost data
 type Repository interface {
 	Has(time.Time, string) (bool, error)
-	Get(time.Time, string) (*model.CustomCostResponse, error)
+	Get(time.Time, string) (*pb.CustomCostResponse, error)
 	Keys() ([]string, error)
-	Put(*model.CustomCostResponse) error
+	Put(*pb.CustomCostResponse) error
 	Expire(time.Time) error
 }

+ 154 - 0
protos/customcost/messages.proto

@@ -0,0 +1,154 @@
+syntax = "proto3";
+
+package customcost.messages;
+
+import "google/protobuf/timestamp.proto";
+import "google/protobuf/duration.proto";
+// Sets the golang package for the protobuf generated code
+option go_package = "github.com/opencost/opencost/core/pkg/model/pb";
+
+// see design at https://link.excalidraw.com/l/ABLQ24dkKai/CBEQtjH6Mr
+// for additional details on how these objects work in the context of
+// opencost's plugin system
+
+message CustomCostRequest {
+  // the window of the returned objects
+  google.protobuf.Timestamp start = 1;
+  google.protobuf.Timestamp end = 2;
+
+  // resolution of steps to return
+  google.protobuf.Duration resolution = 3;
+
+}
+
+message CustomCostResponseSet {
+  repeated CustomCostResponse resps = 1;
+}
+
+message CustomCostResponse {
+  // provides metadata on the Custom CostResponse
+  // deliberately left unstructured
+  map<string, string>  metadata = 1;
+  // declared by plugin
+  // eg snowflake == "data management",
+  // datadog == "observability" etc
+  // intended for top level agg
+  string cost_source = 2;
+  // the name of the custom cost source
+  // e.g., "datadog"
+  string domain = 3;
+  // the version of the Custom Cost response
+  // is set by the plugin, will vary between
+  // different plugins
+  string version = 4;
+  // FOCUS billing currency
+  string currency = 5;
+  // the window of the returned objects
+  google.protobuf.Timestamp start = 6;
+  google.protobuf.Timestamp end = 7;
+
+  // array of CustomCosts
+  repeated CustomCost costs = 8;
+  // any errors in processing
+  repeated string errors = 9;
+}
+
+message CustomCost {
+  // provides metadata on the Custom CostResponse
+  // deliberately left unstructured
+  map<string, string>  metadata = 1;
+  // the region that the resource was incurred
+  // corresponds to 'availability zone' of FOCUS
+  string zone = 2;
+  // FOCUS billing account name
+  string account_name = 3;
+  // FOCUS charge category
+  string charge_category = 4;
+  // FOCUS charge description
+  string description = 5;
+  // FOCUS Resource Name
+  string resource_name = 6;
+  // FOCUS Resource type
+  // if not set, assumed to be domain
+  string resource_type = 7;
+  // ID of the individual cost. should be globally
+  // unique. Assigned by plugin on read
+  string id = 8;
+  // the provider's ID for the cost, if
+  // available
+  // FOCUS resource ID
+  string provider_id = 9;
+
+  // FOCUS billed Cost
+  float billed_cost = 10;
+  // FOCUS List Cost
+  float list_cost = 11;
+  // FOCUS List Unit Price
+  float list_unit_price = 12;
+  // FOCUS usage quantity
+  float usage_quantity = 13;
+  // FOCUS usage Unit
+  string usage_unit = 14;
+  // Returns key/value sets of labels
+  // equivalent to Tags in focus spec
+  map<string, string> labels = 15;
+  // Optional struct to implement other focus
+  // spec attributes
+  optional CustomCostExtendedAttributes extended_attributes = 16;
+
+}
+
+message CustomCostExtendedAttributes {
+  // FOCUS billing period start
+  optional google.protobuf.Timestamp billing_period_start = 1;
+  // FOCUS billing period end
+  optional google.protobuf.Timestamp billing_period_end = 2;
+  // FOCUS Billing Account ID
+  optional string account_id = 3;
+  // FOCUS Charge Frequency
+  optional string charge_frequency = 4;
+  // FOCUS Charge Subcategory
+  optional string subcategory = 5;
+  // FOCUS Commitment Discount Category
+  optional string commitment_discount_category = 6;
+  // FOCUS Commitment Discount ID
+  optional string commitment_discount_id = 7;
+  // FOCUS Commitment Discount Name
+  optional string commitment_discount_name = 8;
+  // FOCUS Commitment Discount Type
+  optional string commitment_discount_type = 9;
+  // FOCUS Effective Cost
+  optional float effective_cost = 10;
+  // FOCUS Invoice Issuer
+  optional string invoice_issuer = 11;
+  // FOCUS Provider
+  // if unset, assumed to be domain
+  optional string provider = 12;
+  // FOCUS Publisher
+  // if unset, assumed to be domain
+  optional string publisher = 13;
+  // FOCUS Service Category
+  // if unset, assumed to be cost source
+  optional string service_category = 14;
+  // FOCUS Service Name
+  // if unset, assumed to be cost source
+  optional string service_name = 15;
+  // FOCUS SKU ID
+  optional string sku_id = 16;
+  // FOCUS SKU Price ID
+  optional string sku_price_id = 17;
+  // FOCUS Sub Account ID
+  optional string sub_account_id = 18;
+  // FOCUS Sub Account Name
+  optional string sub_account_name = 19;
+  // FOCUS Pricing Quantity
+  optional float pricing_quantity = 20;
+  // FOCUS Pricing Unit
+  optional string pricing_unit = 21;
+  // FOCUS Pricing Category
+  optional string pricing_category = 22;
+}
+
+service CustomCostsSource {
+    rpc GetCustomCosts(CustomCostRequest) returns (CustomCostResponseSet);
+}