diff --git a/osac-metering/metering-service/buf.gen.yaml b/osac-metering/metering-service/buf.gen.yaml index 7ecdfe24e..b6125e283 100644 --- a/osac-metering/metering-service/buf.gen.yaml +++ b/osac-metering/metering-service/buf.gen.yaml @@ -25,7 +25,7 @@ managed: inputs: -- module: buf.build/osac-project/private-api:v0.0.83 +- module: buf.build/osac-project/private-api:v0.0.84 plugins: diff --git a/osac-metering/metering-service/cmd/metering-service/main.go b/osac-metering/metering-service/cmd/metering-service/main.go index cf2c518ae..8e02f8907 100644 --- a/osac-metering/metering-service/cmd/metering-service/main.go +++ b/osac-metering/metering-service/cmd/metering-service/main.go @@ -206,7 +206,8 @@ func run(ctx context.Context, logger logr.Logger, cfg *config) error { publisher := kafkapub.NewPublisher(producer) computeClient := privatev1.NewComputeInstancesClient(grpcConn) - reconciler := reconciliation.NewReconciler(computeClient, store, publisher, logger, cfg.heartbeatInterval) + clusterClient := privatev1.NewClustersClient(grpcConn) + reconciler := reconciliation.NewReconciler(computeClient, clusterClient, store, publisher, logger, cfg.heartbeatInterval) logger.Info("running startup reconciliation") if err := reconciler.Reconcile(ctx); err != nil { diff --git a/osac-metering/metering-service/go.mod b/osac-metering/metering-service/go.mod index c75b82151..cd0f341a8 100644 --- a/osac-metering/metering-service/go.mod +++ b/osac-metering/metering-service/go.mod @@ -9,6 +9,7 @@ require ( github.com/go-logr/logr v1.4.4 github.com/go-logr/zapr v1.3.0 github.com/golang-migrate/migrate/v4 v4.19.1 + github.com/google/uuid v1.6.0 github.com/jackc/pgx/v5 v5.10.0 github.com/onsi/ginkgo/v2 v2.32.0 github.com/onsi/gomega v1.42.1 @@ -31,7 +32,6 @@ require ( github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect - github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -47,6 +47,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect github.com/pierrec/lz4/v4 v4.1.27 // indirect github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.70.1 // indirect @@ -55,6 +56,7 @@ require ( github.com/rogpeppe/go-internal v1.15.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/crypto v0.54.0 // indirect diff --git a/osac-metering/metering-service/go.sum b/osac-metering/metering-service/go.sum index 0744716d6..aa580131a 100644 --- a/osac-metering/metering-service/go.sum +++ b/osac-metering/metering-service/go.sum @@ -104,6 +104,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= @@ -130,8 +132,8 @@ github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I= github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= -github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= github.com/pierrec/lz4/v4 v4.1.27 h1:+PhzhWDrjRj89TH2sw43nE3+4+W8lSxIuQadEHZyjUk= github.com/pierrec/lz4/v4 v4.1.27/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -181,8 +183,8 @@ github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gi github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 h1:8tvICD4vSTOOsNrsI4Ljf6C+6UKvpTEH5XY3JMoyPoo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0/go.mod h1:z9+yiacE0IHRqM4qFfkbt/JYlmYXgss8GY/jXoNuPJI= go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= diff --git a/osac-metering/metering-service/internal/api/osac/private/v1/baremetal_instance_type.pb.go b/osac-metering/metering-service/internal/api/osac/private/v1/baremetal_instance_type.pb.go index 1211fcb20..1bb654d69 100644 --- a/osac-metering/metering-service/internal/api/osac/private/v1/baremetal_instance_type.pb.go +++ b/osac-metering/metering-service/internal/api/osac/private/v1/baremetal_instance_type.pb.go @@ -567,9 +567,12 @@ type BareMetalInstanceStatus struct { // The controller sets this to match `spec.restart_trigger` once the power cycle is complete. RestartTrigger int64 `protobuf:"varint,3,opt,name=restart_trigger,json=restartTrigger,proto3" json:"restart_trigger,omitempty"` // Identifier of the hub that was selected for this bare metal instance. - Hub string `protobuf:"bytes,4,opt,name=hub,proto3" json:"hub,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + Hub string `protobuf:"bytes,4,opt,name=hub,proto3" json:"hub,omitempty"` + // Runtime networking state for each network attachment. + // Populated by the operator after DHCP lease discovery and synced via the feedback controller. + NetworkAttachmentStatuses []*BareMetalNetworkAttachmentStatus `protobuf:"bytes,5,rep,name=network_attachment_statuses,json=networkAttachmentStatuses,proto3" json:"network_attachment_statuses,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *BareMetalInstanceStatus) Reset() { @@ -630,6 +633,86 @@ func (x *BareMetalInstanceStatus) GetHub() string { return "" } +func (x *BareMetalInstanceStatus) GetNetworkAttachmentStatuses() []*BareMetalNetworkAttachmentStatus { + if x != nil { + return x.NetworkAttachmentStatuses + } + return nil +} + +// Runtime networking state for a single bare metal network attachment. +type BareMetalNetworkAttachmentStatus struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Physical interface name from the spec's network attachment. + Interface string `protobuf:"bytes,1,opt,name=interface,proto3" json:"interface,omitempty"` + // Reference to the Subnet associated with this attachment. + SubnetRef string `protobuf:"bytes,2,opt,name=subnet_ref,json=subnetRef,proto3" json:"subnet_ref,omitempty"` + // IP address discovered after DHCP assignment. + IpAddress string `protobuf:"bytes,3,opt,name=ip_address,json=ipAddress,proto3" json:"ip_address,omitempty"` + // Whether this attachment is the primary (default gateway) attachment. + Primary bool `protobuf:"varint,4,opt,name=primary,proto3" json:"primary,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BareMetalNetworkAttachmentStatus) Reset() { + *x = BareMetalNetworkAttachmentStatus{} + mi := &file_osac_private_v1_baremetal_instance_type_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BareMetalNetworkAttachmentStatus) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BareMetalNetworkAttachmentStatus) ProtoMessage() {} + +func (x *BareMetalNetworkAttachmentStatus) ProtoReflect() protoreflect.Message { + mi := &file_osac_private_v1_baremetal_instance_type_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BareMetalNetworkAttachmentStatus.ProtoReflect.Descriptor instead. +func (*BareMetalNetworkAttachmentStatus) Descriptor() ([]byte, []int) { + return file_osac_private_v1_baremetal_instance_type_proto_rawDescGZIP(), []int{5} +} + +func (x *BareMetalNetworkAttachmentStatus) GetInterface() string { + if x != nil { + return x.Interface + } + return "" +} + +func (x *BareMetalNetworkAttachmentStatus) GetSubnetRef() string { + if x != nil { + return x.SubnetRef + } + return "" +} + +func (x *BareMetalNetworkAttachmentStatus) GetIpAddress() string { + if x != nil { + return x.IpAddress + } + return "" +} + +func (x *BareMetalNetworkAttachmentStatus) GetPrimary() bool { + if x != nil { + return x.Primary + } + return false +} + // Contains the details of a condition that describes the status of a bare metal instance. type BareMetalInstanceCondition struct { state protoimpl.MessageState `protogen:"open.v1"` @@ -644,7 +727,7 @@ type BareMetalInstanceCondition struct { func (x *BareMetalInstanceCondition) Reset() { *x = BareMetalInstanceCondition{} - mi := &file_osac_private_v1_baremetal_instance_type_proto_msgTypes[5] + mi := &file_osac_private_v1_baremetal_instance_type_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -656,7 +739,7 @@ func (x *BareMetalInstanceCondition) String() string { func (*BareMetalInstanceCondition) ProtoMessage() {} func (x *BareMetalInstanceCondition) ProtoReflect() protoreflect.Message { - mi := &file_osac_private_v1_baremetal_instance_type_proto_msgTypes[5] + mi := &file_osac_private_v1_baremetal_instance_type_proto_msgTypes[6] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -669,7 +752,7 @@ func (x *BareMetalInstanceCondition) ProtoReflect() protoreflect.Message { // Deprecated: Use BareMetalInstanceCondition.ProtoReflect.Descriptor instead. func (*BareMetalInstanceCondition) Descriptor() ([]byte, []int) { - return file_osac_private_v1_baremetal_instance_type_proto_rawDescGZIP(), []int{5} + return file_osac_private_v1_baremetal_instance_type_proto_rawDescGZIP(), []int{6} } func (x *BareMetalInstanceCondition) GetType() BareMetalInstanceConditionType { @@ -718,7 +801,7 @@ type BareMetalInstanceLocalReference struct { func (x *BareMetalInstanceLocalReference) Reset() { *x = BareMetalInstanceLocalReference{} - mi := &file_osac_private_v1_baremetal_instance_type_proto_msgTypes[6] + mi := &file_osac_private_v1_baremetal_instance_type_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -730,7 +813,7 @@ func (x *BareMetalInstanceLocalReference) String() string { func (*BareMetalInstanceLocalReference) ProtoMessage() {} func (x *BareMetalInstanceLocalReference) ProtoReflect() protoreflect.Message { - mi := &file_osac_private_v1_baremetal_instance_type_proto_msgTypes[6] + mi := &file_osac_private_v1_baremetal_instance_type_proto_msgTypes[7] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -743,7 +826,7 @@ func (x *BareMetalInstanceLocalReference) ProtoReflect() protoreflect.Message { // Deprecated: Use BareMetalInstanceLocalReference.ProtoReflect.Descriptor instead. func (*BareMetalInstanceLocalReference) Descriptor() ([]byte, []int) { - return file_osac_private_v1_baremetal_instance_type_proto_rawDescGZIP(), []int{6} + return file_osac_private_v1_baremetal_instance_type_proto_rawDescGZIP(), []int{7} } func (x *BareMetalInstanceLocalReference) GetId() string { @@ -773,7 +856,7 @@ type BareMetalInstanceCatalogItemReference struct { func (x *BareMetalInstanceCatalogItemReference) Reset() { *x = BareMetalInstanceCatalogItemReference{} - mi := &file_osac_private_v1_baremetal_instance_type_proto_msgTypes[7] + mi := &file_osac_private_v1_baremetal_instance_type_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -785,7 +868,7 @@ func (x *BareMetalInstanceCatalogItemReference) String() string { func (*BareMetalInstanceCatalogItemReference) ProtoMessage() {} func (x *BareMetalInstanceCatalogItemReference) ProtoReflect() protoreflect.Message { - mi := &file_osac_private_v1_baremetal_instance_type_proto_msgTypes[7] + mi := &file_osac_private_v1_baremetal_instance_type_proto_msgTypes[8] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -798,7 +881,7 @@ func (x *BareMetalInstanceCatalogItemReference) ProtoReflect() protoreflect.Mess // Deprecated: Use BareMetalInstanceCatalogItemReference.ProtoReflect.Descriptor instead. func (*BareMetalInstanceCatalogItemReference) Descriptor() ([]byte, []int) { - return file_osac_private_v1_baremetal_instance_type_proto_rawDescGZIP(), []int{7} + return file_osac_private_v1_baremetal_instance_type_proto_rawDescGZIP(), []int{8} } func (x *BareMetalInstanceCatalogItemReference) GetId() string { @@ -958,7 +1041,7 @@ var file_osac_private_v1_baremetal_instance_type_proto_rawDesc = string([]byte{ 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x69, 0x6d, - 0x61, 0x67, 0x65, 0x22, 0xe0, 0x01, 0x0a, 0x17, 0x42, 0x61, 0x72, 0x65, 0x4d, 0x65, 0x74, 0x61, + 0x61, 0x67, 0x65, 0x22, 0xd3, 0x02, 0x0a, 0x17, 0x42, 0x61, 0x72, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x6c, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x3d, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x27, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, @@ -972,113 +1055,130 @@ var file_osac_private_v1_baremetal_instance_type_proto_rawDesc = string([]byte{ 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x54, 0x72, 0x69, 0x67, 0x67, 0x65, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x68, 0x75, 0x62, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x03, 0x68, 0x75, 0x62, 0x22, 0xbc, 0x02, 0x0a, 0x1a, 0x42, 0x61, 0x72, 0x65, 0x4d, - 0x65, 0x74, 0x61, 0x6c, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x64, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x43, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x2f, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x72, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x6c, 0x49, - 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x38, 0x0a, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x6f, 0x73, 0x61, - 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, - 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x12, 0x4c, 0x0a, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x74, 0x72, 0x61, - 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 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, 0x12, - 0x6c, 0x61, 0x73, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, - 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, - 0x1d, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, - 0x48, 0x01, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x88, 0x01, 0x01, 0x42, 0x09, - 0x0a, 0x07, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x65, - 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x45, 0x0a, 0x1f, 0x42, 0x61, 0x72, 0x65, 0x4d, 0x65, 0x74, - 0x61, 0x6c, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x52, - 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x7d, 0x0a, 0x25, - 0x42, 0x61, 0x72, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x6c, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, - 0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x66, 0x65, - 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, - 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, - 0x65, 0x63, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x08, 0x52, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2a, 0xaa, 0x01, 0x0a, 0x1c, - 0x42, 0x61, 0x72, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x6c, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, - 0x65, 0x52, 0x75, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x30, 0x0a, 0x2c, + 0x09, 0x52, 0x03, 0x68, 0x75, 0x62, 0x12, 0x71, 0x0a, 0x1b, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x5f, 0x61, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x74, 0x61, + 0x74, 0x75, 0x73, 0x65, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x6f, 0x73, + 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, + 0x72, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x6c, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x74, + 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x19, + 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, + 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x65, 0x73, 0x22, 0x98, 0x01, 0x0a, 0x20, 0x42, 0x61, + 0x72, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x6c, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x74, + 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1c, + 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x66, 0x61, 0x63, 0x65, 0x12, 0x1d, 0x0a, 0x0a, + 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, 0x72, 0x65, 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x09, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x52, 0x65, 0x66, 0x12, 0x1d, 0x0a, 0x0a, 0x69, + 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x69, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, + 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x70, 0x72, 0x69, + 0x6d, 0x61, 0x72, 0x79, 0x22, 0xbc, 0x02, 0x0a, 0x1a, 0x42, 0x61, 0x72, 0x65, 0x4d, 0x65, 0x74, + 0x61, 0x6c, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x43, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x2f, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x72, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x6c, 0x49, 0x6e, 0x73, + 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, + 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x38, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, + 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x12, 0x4c, 0x0a, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 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, 0x12, 0x6c, 0x61, + 0x73, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, + 0x12, 0x1b, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, + 0x48, 0x00, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, + 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, + 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, + 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x22, 0x45, 0x0a, 0x1f, 0x42, 0x61, 0x72, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x6c, + 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x52, 0x65, 0x66, + 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x7d, 0x0a, 0x25, 0x42, 0x61, + 0x72, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x6c, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, + 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, + 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2a, 0xaa, 0x01, 0x0a, 0x1c, 0x42, 0x61, + 0x72, 0x65, 0x4d, 0x65, 0x74, 0x61, 0x6c, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x52, + 0x75, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x30, 0x0a, 0x2c, 0x42, 0x41, + 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, + 0x45, 0x5f, 0x52, 0x55, 0x4e, 0x5f, 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, 0x47, 0x59, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x2b, 0x0a, 0x27, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x52, 0x55, 0x4e, 0x5f, 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, 0x47, 0x59, - 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x2b, - 0x0a, 0x27, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, - 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x52, 0x55, 0x4e, 0x5f, 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, - 0x47, 0x59, 0x5f, 0x41, 0x4c, 0x57, 0x41, 0x59, 0x53, 0x10, 0x01, 0x12, 0x2b, 0x0a, 0x27, 0x42, - 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, - 0x43, 0x45, 0x5f, 0x52, 0x55, 0x4e, 0x5f, 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, 0x47, 0x59, 0x5f, - 0x48, 0x41, 0x4c, 0x54, 0x45, 0x44, 0x10, 0x02, 0x2a, 0xdb, 0x02, 0x0a, 0x16, 0x42, 0x61, 0x72, - 0x65, 0x4d, 0x65, 0x74, 0x61, 0x6c, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x12, 0x29, 0x0a, 0x25, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, - 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, - 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x2a, - 0x0a, 0x26, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, - 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x56, - 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x25, 0x0a, 0x21, 0x42, 0x41, - 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, - 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x52, 0x55, 0x4e, 0x4e, 0x49, 0x4e, 0x47, 0x10, - 0x02, 0x12, 0x24, 0x0a, 0x20, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, - 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x46, - 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x12, 0x26, 0x0a, 0x22, 0x42, 0x41, 0x52, 0x45, 0x5f, - 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, - 0x54, 0x41, 0x54, 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x04, 0x12, - 0x26, 0x0a, 0x22, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, - 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x54, 0x41, - 0x52, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x05, 0x12, 0x26, 0x0a, 0x22, 0x42, 0x41, 0x52, 0x45, 0x5f, - 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, - 0x54, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x49, 0x4e, 0x47, 0x10, 0x06, 0x12, - 0x25, 0x0a, 0x21, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, - 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x54, 0x4f, - 0x50, 0x50, 0x45, 0x44, 0x10, 0x07, 0x2a, 0xa0, 0x03, 0x0a, 0x1e, 0x42, 0x61, 0x72, 0x65, 0x4d, - 0x65, 0x74, 0x61, 0x6c, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x64, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x32, 0x0a, 0x2e, 0x42, 0x41, 0x52, + 0x5f, 0x41, 0x4c, 0x57, 0x41, 0x59, 0x53, 0x10, 0x01, 0x12, 0x2b, 0x0a, 0x27, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, - 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, - 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x32, 0x0a, - 0x2e, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, - 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, - 0x59, 0x50, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x45, 0x44, 0x10, - 0x01, 0x12, 0x3c, 0x0a, 0x38, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, - 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, - 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x55, 0x52, - 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x50, 0x50, 0x4c, 0x49, 0x45, 0x44, 0x10, 0x02, 0x12, - 0x2c, 0x0a, 0x28, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, + 0x5f, 0x52, 0x55, 0x4e, 0x5f, 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, 0x47, 0x59, 0x5f, 0x48, 0x41, + 0x4c, 0x54, 0x45, 0x44, 0x10, 0x02, 0x2a, 0xdb, 0x02, 0x0a, 0x16, 0x42, 0x61, 0x72, 0x65, 0x4d, + 0x65, 0x74, 0x61, 0x6c, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x29, 0x0a, 0x25, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, + 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x2a, 0x0a, 0x26, + 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, + 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, + 0x49, 0x4f, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x25, 0x0a, 0x21, 0x42, 0x41, 0x52, 0x45, + 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, + 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x52, 0x55, 0x4e, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, + 0x24, 0x0a, 0x20, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, + 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x46, 0x41, 0x49, + 0x4c, 0x45, 0x44, 0x10, 0x03, 0x12, 0x26, 0x0a, 0x22, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, + 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, + 0x54, 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x04, 0x12, 0x26, 0x0a, + 0x22, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, + 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, + 0x49, 0x4e, 0x47, 0x10, 0x05, 0x12, 0x26, 0x0a, 0x22, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, + 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, + 0x54, 0x45, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x49, 0x4e, 0x47, 0x10, 0x06, 0x12, 0x25, 0x0a, + 0x21, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, + 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, + 0x45, 0x44, 0x10, 0x07, 0x2a, 0xa0, 0x03, 0x0a, 0x1e, 0x42, 0x61, 0x72, 0x65, 0x4d, 0x65, 0x74, + 0x61, 0x6c, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x32, 0x0a, 0x2e, 0x42, 0x41, 0x52, 0x45, 0x5f, + 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x43, + 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, + 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x32, 0x0a, 0x2e, 0x42, + 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, + 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x45, 0x44, 0x10, 0x01, 0x12, + 0x3c, 0x0a, 0x38, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, - 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, 0x03, 0x12, 0x3a, 0x0a, - 0x36, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x55, 0x52, 0x41, 0x54, + 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x50, 0x50, 0x4c, 0x49, 0x45, 0x44, 0x10, 0x02, 0x12, 0x2c, 0x0a, + 0x28, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, - 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x49, 0x4e, 0x5f, 0x50, - 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x04, 0x12, 0x35, 0x0a, 0x31, 0x42, 0x41, 0x52, - 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, - 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, - 0x52, 0x45, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x05, - 0x12, 0x37, 0x0a, 0x33, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, - 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, - 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x52, - 0x45, 0x51, 0x55, 0x49, 0x52, 0x45, 0x44, 0x10, 0x06, 0x42, 0xdf, 0x01, 0x0a, 0x13, 0x63, 0x6f, - 0x6d, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, - 0x31, 0x42, 0x1a, 0x42, 0x61, 0x72, 0x65, 0x6d, 0x65, 0x74, 0x61, 0x6c, 0x49, 0x6e, 0x73, 0x74, - 0x61, 0x6e, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, - 0x4c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x73, 0x61, 0x63, - 0x2d, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x6d, 0x65, - 0x74, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, - 0x61, 0x70, 0x69, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x2f, 0x76, 0x31, 0x3b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, - 0x4f, 0x50, 0x58, 0xaa, 0x02, 0x0f, 0x4f, 0x73, 0x61, 0x63, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x10, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1c, 0x4f, 0x73, 0x61, 0x63, 0x5c, - 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x4f, 0x73, 0x61, 0x63, 0x3a, 0x3a, - 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, 0x03, 0x12, 0x3a, 0x0a, 0x36, 0x42, + 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, + 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x52, 0x45, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x49, 0x4e, 0x5f, 0x50, 0x52, 0x4f, + 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x04, 0x12, 0x35, 0x0a, 0x31, 0x42, 0x41, 0x52, 0x45, 0x5f, + 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x43, + 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, + 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x12, 0x37, + 0x0a, 0x33, 0x42, 0x41, 0x52, 0x45, 0x5f, 0x4d, 0x45, 0x54, 0x41, 0x4c, 0x5f, 0x49, 0x4e, 0x53, + 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, + 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x52, 0x45, 0x51, + 0x55, 0x49, 0x52, 0x45, 0x44, 0x10, 0x06, 0x42, 0xdf, 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, + 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x42, + 0x1a, 0x42, 0x61, 0x72, 0x65, 0x6d, 0x65, 0x74, 0x61, 0x6c, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, + 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4c, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x70, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x6d, 0x65, 0x74, 0x65, + 0x72, 0x69, 0x6e, 0x67, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, + 0x69, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, + 0x31, 0x3b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4f, 0x50, + 0x58, 0xaa, 0x02, 0x0f, 0x4f, 0x73, 0x61, 0x63, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x2e, 0x56, 0x31, 0xca, 0x02, 0x10, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1c, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x4f, 0x73, 0x61, 0x63, 0x3a, 0x3a, 0x50, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, }) var ( @@ -1094,7 +1194,7 @@ func file_osac_private_v1_baremetal_instance_type_proto_rawDescGZIP() []byte { } var file_osac_private_v1_baremetal_instance_type_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_osac_private_v1_baremetal_instance_type_proto_msgTypes = make([]protoimpl.MessageInfo, 9) +var file_osac_private_v1_baremetal_instance_type_proto_msgTypes = make([]protoimpl.MessageInfo, 10) var file_osac_private_v1_baremetal_instance_type_proto_goTypes = []any{ (BareMetalInstanceRunStrategy)(0), // 0: osac.private.v1.BareMetalInstanceRunStrategy (BareMetalInstanceState)(0), // 1: osac.private.v1.BareMetalInstanceState @@ -1104,39 +1204,41 @@ var file_osac_private_v1_baremetal_instance_type_proto_goTypes = []any{ (*BareMetalNetworkAttachment)(nil), // 5: osac.private.v1.BareMetalNetworkAttachment (*BareMetalInstanceSpec)(nil), // 6: osac.private.v1.BareMetalInstanceSpec (*BareMetalInstanceStatus)(nil), // 7: osac.private.v1.BareMetalInstanceStatus - (*BareMetalInstanceCondition)(nil), // 8: osac.private.v1.BareMetalInstanceCondition - (*BareMetalInstanceLocalReference)(nil), // 9: osac.private.v1.BareMetalInstanceLocalReference - (*BareMetalInstanceCatalogItemReference)(nil), // 10: osac.private.v1.BareMetalInstanceCatalogItemReference - nil, // 11: osac.private.v1.BareMetalInstanceSpec.TemplateParametersEntry - (*Metadata)(nil), // 12: osac.private.v1.Metadata - (*SubnetLocalReference)(nil), // 13: osac.private.v1.SubnetLocalReference - (*SecurityGroupLocalReference)(nil), // 14: osac.private.v1.SecurityGroupLocalReference - (ConditionStatus)(0), // 15: osac.private.v1.ConditionStatus - (*timestamppb.Timestamp)(nil), // 16: google.protobuf.Timestamp - (*anypb.Any)(nil), // 17: google.protobuf.Any + (*BareMetalNetworkAttachmentStatus)(nil), // 8: osac.private.v1.BareMetalNetworkAttachmentStatus + (*BareMetalInstanceCondition)(nil), // 9: osac.private.v1.BareMetalInstanceCondition + (*BareMetalInstanceLocalReference)(nil), // 10: osac.private.v1.BareMetalInstanceLocalReference + (*BareMetalInstanceCatalogItemReference)(nil), // 11: osac.private.v1.BareMetalInstanceCatalogItemReference + nil, // 12: osac.private.v1.BareMetalInstanceSpec.TemplateParametersEntry + (*Metadata)(nil), // 13: osac.private.v1.Metadata + (*SubnetLocalReference)(nil), // 14: osac.private.v1.SubnetLocalReference + (*SecurityGroupLocalReference)(nil), // 15: osac.private.v1.SecurityGroupLocalReference + (ConditionStatus)(0), // 16: osac.private.v1.ConditionStatus + (*timestamppb.Timestamp)(nil), // 17: google.protobuf.Timestamp + (*anypb.Any)(nil), // 18: google.protobuf.Any } var file_osac_private_v1_baremetal_instance_type_proto_depIdxs = []int32{ - 12, // 0: osac.private.v1.BareMetalInstance.metadata:type_name -> osac.private.v1.Metadata + 13, // 0: osac.private.v1.BareMetalInstance.metadata:type_name -> osac.private.v1.Metadata 6, // 1: osac.private.v1.BareMetalInstance.spec:type_name -> osac.private.v1.BareMetalInstanceSpec 7, // 2: osac.private.v1.BareMetalInstance.status:type_name -> osac.private.v1.BareMetalInstanceStatus - 13, // 3: osac.private.v1.BareMetalNetworkAttachment.subnet:type_name -> osac.private.v1.SubnetLocalReference - 14, // 4: osac.private.v1.BareMetalNetworkAttachment.security_groups:type_name -> osac.private.v1.SecurityGroupLocalReference - 10, // 5: osac.private.v1.BareMetalInstanceSpec.catalog_item:type_name -> osac.private.v1.BareMetalInstanceCatalogItemReference + 14, // 3: osac.private.v1.BareMetalNetworkAttachment.subnet:type_name -> osac.private.v1.SubnetLocalReference + 15, // 4: osac.private.v1.BareMetalNetworkAttachment.security_groups:type_name -> osac.private.v1.SecurityGroupLocalReference + 11, // 5: osac.private.v1.BareMetalInstanceSpec.catalog_item:type_name -> osac.private.v1.BareMetalInstanceCatalogItemReference 0, // 6: osac.private.v1.BareMetalInstanceSpec.run_strategy:type_name -> osac.private.v1.BareMetalInstanceRunStrategy - 11, // 7: osac.private.v1.BareMetalInstanceSpec.template_parameters:type_name -> osac.private.v1.BareMetalInstanceSpec.TemplateParametersEntry + 12, // 7: osac.private.v1.BareMetalInstanceSpec.template_parameters:type_name -> osac.private.v1.BareMetalInstanceSpec.TemplateParametersEntry 3, // 8: osac.private.v1.BareMetalInstanceSpec.image:type_name -> osac.private.v1.BareMetalInstanceImage 5, // 9: osac.private.v1.BareMetalInstanceSpec.network_attachments:type_name -> osac.private.v1.BareMetalNetworkAttachment 1, // 10: osac.private.v1.BareMetalInstanceStatus.state:type_name -> osac.private.v1.BareMetalInstanceState - 8, // 11: osac.private.v1.BareMetalInstanceStatus.conditions:type_name -> osac.private.v1.BareMetalInstanceCondition - 2, // 12: osac.private.v1.BareMetalInstanceCondition.type:type_name -> osac.private.v1.BareMetalInstanceConditionType - 15, // 13: osac.private.v1.BareMetalInstanceCondition.status:type_name -> osac.private.v1.ConditionStatus - 16, // 14: osac.private.v1.BareMetalInstanceCondition.last_transition_time:type_name -> google.protobuf.Timestamp - 17, // 15: osac.private.v1.BareMetalInstanceSpec.TemplateParametersEntry.value:type_name -> google.protobuf.Any - 16, // [16:16] is the sub-list for method output_type - 16, // [16:16] is the sub-list for method input_type - 16, // [16:16] is the sub-list for extension type_name - 16, // [16:16] is the sub-list for extension extendee - 0, // [0:16] is the sub-list for field type_name + 9, // 11: osac.private.v1.BareMetalInstanceStatus.conditions:type_name -> osac.private.v1.BareMetalInstanceCondition + 8, // 12: osac.private.v1.BareMetalInstanceStatus.network_attachment_statuses:type_name -> osac.private.v1.BareMetalNetworkAttachmentStatus + 2, // 13: osac.private.v1.BareMetalInstanceCondition.type:type_name -> osac.private.v1.BareMetalInstanceConditionType + 16, // 14: osac.private.v1.BareMetalInstanceCondition.status:type_name -> osac.private.v1.ConditionStatus + 17, // 15: osac.private.v1.BareMetalInstanceCondition.last_transition_time:type_name -> google.protobuf.Timestamp + 18, // 16: osac.private.v1.BareMetalInstanceSpec.TemplateParametersEntry.value:type_name -> google.protobuf.Any + 17, // [17:17] is the sub-list for method output_type + 17, // [17:17] is the sub-list for method input_type + 17, // [17:17] is the sub-list for extension type_name + 17, // [17:17] is the sub-list for extension extendee + 0, // [0:17] is the sub-list for field type_name } func init() { file_osac_private_v1_baremetal_instance_type_proto_init() } @@ -1150,14 +1252,14 @@ func file_osac_private_v1_baremetal_instance_type_proto_init() { file_osac_private_v1_subnet_type_proto_init() file_osac_private_v1_baremetal_instance_type_proto_msgTypes[2].OneofWrappers = []any{} file_osac_private_v1_baremetal_instance_type_proto_msgTypes[3].OneofWrappers = []any{} - file_osac_private_v1_baremetal_instance_type_proto_msgTypes[5].OneofWrappers = []any{} + file_osac_private_v1_baremetal_instance_type_proto_msgTypes[6].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_osac_private_v1_baremetal_instance_type_proto_rawDesc), len(file_osac_private_v1_baremetal_instance_type_proto_rawDesc)), NumEnums: 3, - NumMessages: 9, + NumMessages: 10, NumExtensions: 0, NumServices: 0, }, diff --git a/osac-metering/metering-service/internal/api/osac/private/v1/cluster_template_type.pb.go b/osac-metering/metering-service/internal/api/osac/private/v1/cluster_template_type.pb.go index 15540bc09..4facfccc5 100644 --- a/osac-metering/metering-service/internal/api/osac/private/v1/cluster_template_type.pb.go +++ b/osac-metering/metering-service/internal/api/osac/private/v1/cluster_template_type.pb.go @@ -278,8 +278,8 @@ type ClusterTemplateSpecDefaults struct { SshPublicKey *string `protobuf:"bytes,2,opt,name=ssh_public_key,json=sshPublicKey,proto3,oneof" json:"ssh_public_key,omitempty"` // Default cluster networking configuration. Network *ClusterNetwork `protobuf:"bytes,4,opt,name=network,proto3,oneof" json:"network,omitempty"` - // Default ClusterVersion name for clusters created with this template. - VersionName *string `protobuf:"bytes,5,opt,name=version_name,json=versionName,proto3,oneof" json:"version_name,omitempty"` + // Default ClusterVersion for clusters created with this template. + Version *ClusterVersionReference `protobuf:"bytes,5,opt,name=version,proto3" json:"version,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -335,11 +335,11 @@ func (x *ClusterTemplateSpecDefaults) GetNetwork() *ClusterNetwork { return nil } -func (x *ClusterTemplateSpecDefaults) GetVersionName() string { - if x != nil && x.VersionName != nil { - return *x.VersionName +func (x *ClusterTemplateSpecDefaults) GetVersion() *ClusterVersionReference { + if x != nil { + return x.Version } - return "" + return nil } var File_osac_private_v1_cluster_template_type_proto protoreflect.FileDescriptor @@ -352,95 +352,98 @@ var file_osac_private_v1_cluster_template_type_proto_rawDesc = string([]byte{ 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x22, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6c, 0x75, 0x73, 0x74, - 0x65, 0x72, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x23, 0x6f, - 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6d, - 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x1a, 0x24, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x2f, 0x76, 0x31, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x74, 0x79, - 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xeb, 0x03, 0x0a, 0x0f, 0x43, 0x6c, 0x75, - 0x73, 0x74, 0x65, 0x72, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x35, 0x0a, 0x08, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, - 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, - 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x53, 0x0a, 0x0a, 0x70, - 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x33, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, + 0x65, 0x72, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2a, 0x6f, + 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x63, + 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x74, + 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x23, 0x6f, 0x73, 0x61, 0x63, 0x2f, + 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x24, + 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, + 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xeb, 0x03, 0x0a, 0x0f, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, + 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x35, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6f, 0x73, 0x61, + 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, + 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, + 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x53, 0x0a, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, + 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x6f, 0x73, + 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, + 0x75, 0x73, 0x74, 0x65, 0x72, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, + 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x4b, 0x0a, 0x09, + 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x2e, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, - 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, - 0x12, 0x4b, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x06, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x54, 0x65, 0x6d, - 0x70, 0x6c, 0x61, 0x74, 0x65, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 0x73, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 0x73, 0x12, 0x51, 0x0a, - 0x0d, 0x73, 0x70, 0x65, 0x63, 0x5f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, - 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x54, 0x65, - 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x53, 0x70, 0x65, 0x63, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, - 0x74, 0x73, 0x52, 0x0c, 0x73, 0x70, 0x65, 0x63, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, - 0x1a, 0x64, 0x0a, 0x0d, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 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, 0x3d, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, - 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x54, 0x65, 0x6d, 0x70, - 0x6c, 0x61, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xd0, 0x01, 0x0a, 0x22, 0x43, 0x6c, 0x75, 0x73, 0x74, - 0x65, 0x72, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, - 0x74, 0x65, 0x72, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, - 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x71, - 0x75, 0x69, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x71, - 0x75, 0x69, 0x72, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x64, 0x65, 0x66, - 0x61, 0x75, 0x6c, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, - 0x52, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x22, 0x6d, 0x0a, 0x16, 0x43, 0x6c, 0x75, - 0x73, 0x74, 0x65, 0x72, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, - 0x53, 0x65, 0x74, 0x12, 0x3f, 0x0a, 0x09, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x54, 0x79, 0x70, - 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, - 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x22, 0x96, 0x02, 0x0a, 0x1b, 0x43, 0x6c, 0x75, - 0x73, 0x74, 0x65, 0x72, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x53, 0x70, 0x65, 0x63, - 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x24, 0x0a, 0x0b, 0x70, 0x75, 0x6c, 0x6c, - 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, - 0x0a, 0x70, 0x75, 0x6c, 0x6c, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x88, 0x01, 0x01, 0x12, 0x29, - 0x0a, 0x0e, 0x73, 0x73, 0x68, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0c, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, - 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x88, 0x01, 0x01, 0x12, 0x3e, 0x0a, 0x07, 0x6e, 0x65, 0x74, - 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6f, 0x73, 0x61, - 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, - 0x73, 0x74, 0x65, 0x72, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x48, 0x02, 0x52, 0x07, 0x6e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x88, 0x01, 0x01, 0x12, 0x26, 0x0a, 0x0c, 0x76, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, - 0x03, 0x52, 0x0b, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x88, 0x01, - 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x70, 0x75, 0x6c, 0x6c, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, - 0x74, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x73, 0x73, 0x68, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, - 0x5f, 0x6b, 0x65, 0x79, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, - 0x65, 0x42, 0xdd, 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, - 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x18, 0x43, 0x6c, 0x75, 0x73, 0x74, - 0x65, 0x72, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x54, 0x79, 0x70, 0x65, 0x50, 0x72, - 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x6f, - 0x73, 0x61, 0x63, 0x2d, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2f, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, - 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, - 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4f, 0x50, 0x58, 0xaa, 0x02, 0x0f, 0x4f, 0x73, 0x61, 0x63, - 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x10, 0x4f, 0x73, - 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0xe2, 0x02, - 0x1c, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, - 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, - 0x4f, 0x73, 0x61, 0x63, 0x3a, 0x3a, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x3a, 0x3a, 0x56, - 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, + 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 0x73, 0x12, 0x51, 0x0a, 0x0d, 0x73, 0x70, 0x65, + 0x63, 0x5f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x2c, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, + 0x74, 0x65, 0x53, 0x70, 0x65, 0x63, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, 0x52, 0x0c, + 0x73, 0x70, 0x65, 0x63, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, 0x1a, 0x64, 0x0a, 0x0d, + 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 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, + 0x3d, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, + 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, + 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, + 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x22, 0xd0, 0x01, 0x0a, 0x22, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x54, 0x65, + 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x44, + 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, + 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, + 0x74, 0x6c, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, + 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, + 0x64, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2e, 0x0a, 0x07, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x07, 0x64, 0x65, + 0x66, 0x61, 0x75, 0x6c, 0x74, 0x22, 0x6d, 0x0a, 0x16, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, + 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 0x12, + 0x3f, 0x0a, 0x09, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x65, 0x66, + 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, + 0x73, 0x69, 0x7a, 0x65, 0x22, 0xa1, 0x02, 0x0a, 0x1b, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, + 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x53, 0x70, 0x65, 0x63, 0x44, 0x65, 0x66, 0x61, + 0x75, 0x6c, 0x74, 0x73, 0x12, 0x24, 0x0a, 0x0b, 0x70, 0x75, 0x6c, 0x6c, 0x5f, 0x73, 0x65, 0x63, + 0x72, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0a, 0x70, 0x75, 0x6c, + 0x6c, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x88, 0x01, 0x01, 0x12, 0x29, 0x0a, 0x0e, 0x73, 0x73, + 0x68, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x48, 0x01, 0x52, 0x0c, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, + 0x65, 0x79, 0x88, 0x01, 0x01, 0x12, 0x3e, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, + 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x48, 0x02, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x88, 0x01, 0x01, 0x12, 0x42, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, + 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, + 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x70, 0x75, + 0x6c, 0x6c, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x73, 0x73, + 0x68, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x42, 0x0a, 0x0a, 0x08, + 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x42, 0xdd, 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, + 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, + 0x42, 0x18, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, + 0x65, 0x54, 0x79, 0x70, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4c, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x70, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x69, 0x6e, 0x67, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, + 0x3b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4f, 0x50, 0x58, + 0xaa, 0x02, 0x0f, 0x4f, 0x73, 0x61, 0x63, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, + 0x56, 0x31, 0xca, 0x02, 0x10, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x5f, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1c, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x4f, 0x73, 0x61, 0x63, 0x3a, 0x3a, 0x50, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -466,6 +469,7 @@ var file_osac_private_v1_cluster_template_type_proto_goTypes = []any{ (*anypb.Any)(nil), // 6: google.protobuf.Any (*HostTypeReference)(nil), // 7: osac.private.v1.HostTypeReference (*ClusterNetwork)(nil), // 8: osac.private.v1.ClusterNetwork + (*ClusterVersionReference)(nil), // 9: osac.private.v1.ClusterVersionReference } var file_osac_private_v1_cluster_template_type_proto_depIdxs = []int32{ 5, // 0: osac.private.v1.ClusterTemplate.metadata:type_name -> osac.private.v1.Metadata @@ -475,12 +479,13 @@ var file_osac_private_v1_cluster_template_type_proto_depIdxs = []int32{ 6, // 4: osac.private.v1.ClusterTemplateParameterDefinition.default:type_name -> google.protobuf.Any 7, // 5: osac.private.v1.ClusterTemplateNodeSet.host_type:type_name -> osac.private.v1.HostTypeReference 8, // 6: osac.private.v1.ClusterTemplateSpecDefaults.network:type_name -> osac.private.v1.ClusterNetwork - 2, // 7: osac.private.v1.ClusterTemplate.NodeSetsEntry.value:type_name -> osac.private.v1.ClusterTemplateNodeSet - 8, // [8:8] is the sub-list for method output_type - 8, // [8:8] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name + 9, // 7: osac.private.v1.ClusterTemplateSpecDefaults.version:type_name -> osac.private.v1.ClusterVersionReference + 2, // 8: osac.private.v1.ClusterTemplate.NodeSetsEntry.value:type_name -> osac.private.v1.ClusterTemplateNodeSet + 9, // [9:9] is the sub-list for method output_type + 9, // [9:9] is the sub-list for method input_type + 9, // [9:9] is the sub-list for extension type_name + 9, // [9:9] is the sub-list for extension extendee + 0, // [0:9] is the sub-list for field type_name } func init() { file_osac_private_v1_cluster_template_type_proto_init() } @@ -489,6 +494,7 @@ func file_osac_private_v1_cluster_template_type_proto_init() { return } file_osac_private_v1_cluster_type_proto_init() + file_osac_private_v1_cluster_version_type_proto_init() file_osac_private_v1_metadata_type_proto_init() file_osac_private_v1_host_type_type_proto_init() file_osac_private_v1_cluster_template_type_proto_msgTypes[3].OneofWrappers = []any{} diff --git a/osac-metering/metering-service/internal/api/osac/private/v1/cluster_type.pb.go b/osac-metering/metering-service/internal/api/osac/private/v1/cluster_type.pb.go index 684704fc8..74a5eb045 100644 --- a/osac-metering/metering-service/internal/api/osac/private/v1/cluster_type.pb.go +++ b/osac-metering/metering-service/internal/api/osac/private/v1/cluster_type.pb.go @@ -227,7 +227,7 @@ type ClusterSpec struct { NodeSets map[string]*ClusterNodeSet `protobuf:"bytes,3,rep,name=node_sets,json=nodeSets,proto3" json:"node_sets,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` PullSecret *string `protobuf:"bytes,4,opt,name=pull_secret,json=pullSecret,proto3,oneof" json:"pull_secret,omitempty"` SshPublicKey *string `protobuf:"bytes,5,opt,name=ssh_public_key,json=sshPublicKey,proto3,oneof" json:"ssh_public_key,omitempty"` - VersionName *string `protobuf:"bytes,6,opt,name=version_name,json=versionName,proto3,oneof" json:"version_name,omitempty"` + Version *ClusterVersionReference `protobuf:"bytes,6,opt,name=version,proto3" json:"version,omitempty"` Network *ClusterNetwork `protobuf:"bytes,7,opt,name=network,proto3,oneof" json:"network,omitempty"` // Reference to a cluster catalog item. Mutually exclusive with template during the migration period. // When set, the server fetches the catalog item and applies its field definitions. @@ -311,11 +311,11 @@ func (x *ClusterSpec) GetSshPublicKey() string { return "" } -func (x *ClusterSpec) GetVersionName() string { - if x != nil && x.VersionName != nil { - return *x.VersionName +func (x *ClusterSpec) GetVersion() *ClusterVersionReference { + if x != nil { + return x.Version } - return "" + return nil } func (x *ClusterSpec) GetNetwork() *ClusterNetwork { @@ -913,222 +913,225 @@ var file_osac_private_v1_cluster_type_proto_rawDesc = string([]byte{ 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 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, 0x2b, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, - 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x23, - 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, - 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x1a, 0x24, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, - 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x74, - 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x29, 0x6f, 0x73, 0x61, 0x63, 0x2f, - 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x65, 0x63, 0x75, 0x72, - 0x69, 0x74, 0x79, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x21, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, 0x74, 0x79, 0x70, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xba, 0x01, 0x0a, 0x07, 0x43, 0x6c, 0x75, 0x73, - 0x74, 0x65, 0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x02, 0x69, 0x64, 0x12, 0x35, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x30, 0x0a, 0x04, 0x73, 0x70, - 0x65, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, + 0x6f, 0x1a, 0x2a, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, + 0x76, 0x31, 0x2f, 0x63, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2b, 0x6f, + 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x63, + 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5f, + 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x23, 0x6f, 0x73, 0x61, 0x63, + 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, + 0x24, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, + 0x2f, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x29, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x5f, + 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x1a, 0x21, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, + 0x31, 0x2f, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x22, 0xba, 0x01, 0x0a, 0x07, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x12, + 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, + 0x35, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x19, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x30, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x53, 0x70, + 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x12, 0x36, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, - 0x65, 0x72, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x12, 0x36, 0x0a, 0x06, - 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6f, - 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, - 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x22, 0xe3, 0x06, 0x0a, 0x0b, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, - 0x53, 0x70, 0x65, 0x63, 0x12, 0x45, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, - 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, - 0x65, 0x52, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x65, 0x0a, 0x13, 0x74, - 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, - 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, - 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, - 0x65, 0x72, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x50, - 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x12, - 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, - 0x72, 0x73, 0x12, 0x47, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, - 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x53, - 0x70, 0x65, 0x63, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 0x73, 0x12, 0x24, 0x0a, 0x0b, 0x70, - 0x75, 0x6c, 0x6c, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, - 0x48, 0x00, 0x52, 0x0a, 0x70, 0x75, 0x6c, 0x6c, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x88, 0x01, - 0x01, 0x12, 0x29, 0x0a, 0x0e, 0x73, 0x73, 0x68, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, - 0x6b, 0x65, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0c, 0x73, 0x73, 0x68, - 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x88, 0x01, 0x01, 0x12, 0x26, 0x0a, 0x0c, - 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x09, 0x48, 0x02, 0x52, 0x0b, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, - 0x65, 0x88, 0x01, 0x01, 0x12, 0x3e, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x48, 0x03, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x88, 0x01, 0x01, 0x12, 0x4f, 0x0a, 0x0c, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x5f, - 0x69, 0x74, 0x65, 0x6d, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x6f, 0x73, 0x61, - 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, - 0x73, 0x74, 0x65, 0x72, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x52, - 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0b, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, - 0x67, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x58, 0x0a, 0x12, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x5f, 0x61, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x29, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x11, 0x6e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x1a, - 0x5b, 0x0a, 0x17, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, - 0x65, 0x74, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, - 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, - 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x5c, 0x0a, 0x0d, - 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 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, - 0x35, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, - 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x70, - 0x75, 0x6c, 0x6c, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x73, - 0x73, 0x68, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x42, 0x0f, 0x0a, - 0x0d, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x42, 0x0a, - 0x0a, 0x08, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x22, 0x76, 0x0a, 0x0e, 0x43, 0x6c, - 0x75, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x1e, 0x0a, 0x08, - 0x70, 0x6f, 0x64, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, - 0x52, 0x07, 0x70, 0x6f, 0x64, 0x43, 0x69, 0x64, 0x72, 0x88, 0x01, 0x01, 0x12, 0x26, 0x0a, 0x0c, - 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x48, 0x01, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x43, 0x69, 0x64, - 0x72, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x70, 0x6f, 0x64, 0x5f, 0x63, 0x69, 0x64, - 0x72, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x63, 0x69, - 0x64, 0x72, 0x22, 0xb9, 0x04, 0x0a, 0x0d, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x12, 0x33, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x41, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, - 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, + 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x22, 0xee, 0x06, 0x0a, 0x0b, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x53, 0x70, 0x65, 0x63, + 0x12, 0x45, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x54, 0x65, 0x6d, 0x70, + 0x6c, 0x61, 0x74, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x08, 0x74, + 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x65, 0x0a, 0x13, 0x74, 0x65, 0x6d, 0x70, 0x6c, + 0x61, 0x74, 0x65, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x53, 0x70, + 0x65, 0x63, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, + 0x65, 0x74, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x12, 0x74, 0x65, 0x6d, 0x70, + 0x6c, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x47, + 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x2a, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x53, 0x70, 0x65, 0x63, 0x2e, + 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6e, + 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 0x73, 0x12, 0x24, 0x0a, 0x0b, 0x70, 0x75, 0x6c, 0x6c, 0x5f, + 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0a, + 0x70, 0x75, 0x6c, 0x6c, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x88, 0x01, 0x01, 0x12, 0x29, 0x0a, + 0x0e, 0x73, 0x73, 0x68, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0c, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x4b, 0x65, 0x79, 0x88, 0x01, 0x01, 0x12, 0x42, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x6f, 0x73, 0x61, 0x63, + 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, + 0x74, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, + 0x6e, 0x63, 0x65, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x3e, 0x0a, 0x07, + 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x17, 0x0a, 0x07, - 0x61, 0x70, 0x69, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, - 0x70, 0x69, 0x55, 0x72, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, - 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x73, - 0x6f, 0x6c, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x49, 0x0a, 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x73, - 0x65, 0x74, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x6f, 0x73, 0x61, 0x63, + 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x48, 0x02, + 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x88, 0x01, 0x01, 0x12, 0x4f, 0x0a, 0x0c, + 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x08, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x43, 0x61, 0x74, 0x61, + 0x6c, 0x6f, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, + 0x52, 0x0b, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x58, 0x0a, + 0x12, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x61, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, + 0x65, 0x6e, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, - 0x74, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x65, - 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, - 0x73, 0x12, 0x10, 0x0a, 0x03, 0x68, 0x75, 0x62, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x68, 0x75, 0x62, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x70, 0x69, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, - 0x69, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x70, 0x69, 0x45, 0x6e, - 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, - 0x73, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0f, 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, - 0x74, 0x12, 0x53, 0x0a, 0x15, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 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, 0x13, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, - 0x69, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x1a, 0x5c, 0x0a, 0x0d, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x65, - 0x74, 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, 0x35, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, + 0x74, 0x65, 0x72, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, + 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x11, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x74, 0x74, + 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x1a, 0x5b, 0x0a, 0x17, 0x54, 0x65, 0x6d, 0x70, 0x6c, + 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x5c, 0x0a, 0x0d, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 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, 0x35, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, + 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x70, 0x75, 0x6c, 0x6c, 0x5f, 0x73, 0x65, 0x63, 0x72, + 0x65, 0x74, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x73, 0x73, 0x68, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, + 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x22, 0x76, 0x0a, 0x0e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x12, 0x1e, 0x0a, 0x08, 0x70, 0x6f, 0x64, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x07, 0x70, 0x6f, 0x64, 0x43, 0x69, 0x64, 0x72, + 0x88, 0x01, 0x01, 0x12, 0x26, 0x0a, 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x63, + 0x69, 0x64, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0b, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x43, 0x69, 0x64, 0x72, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, + 0x70, 0x6f, 0x64, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x22, 0xb9, 0x04, 0x0a, 0x0d, 0x43, 0x6c, + 0x75, 0x73, 0x74, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x33, 0x0a, 0x05, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x6f, 0x73, 0x61, + 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, + 0x73, 0x74, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x12, 0x41, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x43, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x12, 0x17, 0x0a, 0x07, 0x61, 0x70, 0x69, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x70, 0x69, 0x55, 0x72, 0x6c, 0x12, 0x1f, 0x0a, 0x0b, + 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x49, 0x0a, + 0x09, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x73, 0x65, 0x74, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x2c, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x2e, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, + 0x6e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x68, 0x75, 0x62, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x68, 0x75, 0x62, 0x12, 0x21, 0x0a, 0x0c, 0x61, 0x70, + 0x69, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0b, 0x61, 0x70, 0x69, 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x29, 0x0a, + 0x10, 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, + 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, + 0x45, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x12, 0x53, 0x0a, 0x15, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x09, 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, 0x13, 0x73, 0x74, 0x61, 0x74, 0x65, 0x54, 0x72, 0x61, 0x6e, + 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x1a, 0x5c, 0x0a, + 0x0d, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 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, 0x35, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1f, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x18, 0x0a, 0x16, 0x5f, + 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x22, 0xa8, 0x02, 0x0a, 0x10, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, + 0x72, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x25, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, - 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x3a, 0x02, 0x38, 0x01, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x74, - 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x22, 0xa8, - 0x02, 0x0a, 0x10, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x25, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x64, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x38, - 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, - 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x4c, 0x0a, 0x14, 0x6c, 0x61, 0x73, 0x74, - 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, - 0x18, 0x03, 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, 0x12, 0x6c, 0x61, 0x73, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, - 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, - 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x88, - 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x42, 0x0a, 0x0a, - 0x08, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0xb0, 0x01, 0x0a, 0x18, 0x43, 0x6c, - 0x75, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, 0x74, 0x74, 0x61, - 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x3d, 0x0a, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x4c, - 0x6f, 0x63, 0x61, 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x06, 0x73, - 0x75, 0x62, 0x6e, 0x65, 0x74, 0x12, 0x55, 0x0a, 0x0f, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, - 0x79, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, - 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x4c, 0x6f, - 0x63, 0x61, 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0e, 0x73, 0x65, - 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x22, 0x65, 0x0a, 0x0e, - 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x64, 0x65, 0x53, 0x65, 0x74, 0x12, 0x3f, - 0x0a, 0x09, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x22, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x52, 0x65, 0x66, 0x65, - 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x73, - 0x69, 0x7a, 0x65, 0x22, 0x3b, 0x0a, 0x15, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4c, 0x6f, - 0x63, 0x61, 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x22, 0x70, 0x0a, 0x18, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x54, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, - 0x61, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x73, 0x68, 0x61, 0x72, - 0x65, 0x64, 0x22, 0x73, 0x0a, 0x1b, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x43, 0x61, 0x74, - 0x61, 0x6c, 0x6f, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, - 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, - 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, - 0x16, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2a, 0xbc, 0x01, 0x0a, 0x0c, 0x43, 0x6c, 0x75, 0x73, - 0x74, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1d, 0x0a, 0x19, 0x43, 0x4c, 0x55, 0x53, - 0x54, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, - 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1d, 0x0a, 0x19, 0x43, 0x4c, 0x55, 0x53, 0x54, - 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, - 0x53, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x17, 0x0a, 0x13, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, - 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, 0x02, 0x12, - 0x18, 0x0a, 0x14, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, - 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x12, 0x1a, 0x0a, 0x16, 0x43, 0x4c, 0x55, - 0x53, 0x54, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, - 0x49, 0x4e, 0x47, 0x10, 0x04, 0x12, 0x1f, 0x0a, 0x1b, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, - 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x46, 0x41, - 0x49, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x2a, 0xd0, 0x01, 0x0a, 0x14, 0x43, 0x6c, 0x75, 0x73, 0x74, - 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, - 0x26, 0x0a, 0x22, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, - 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, - 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x26, 0x0a, 0x22, 0x43, 0x4c, 0x55, 0x53, 0x54, - 0x45, 0x52, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, - 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, 0x53, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, - 0x20, 0x0a, 0x1c, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, - 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, - 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x43, 0x4f, 0x4e, - 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, - 0x45, 0x44, 0x10, 0x03, 0x12, 0x23, 0x0a, 0x1f, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x5f, - 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, - 0x45, 0x47, 0x52, 0x41, 0x44, 0x45, 0x44, 0x10, 0x04, 0x42, 0xd5, 0x01, 0x0a, 0x13, 0x63, 0x6f, - 0x6d, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, - 0x31, 0x42, 0x10, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x50, 0x72, - 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x6f, - 0x73, 0x61, 0x63, 0x2d, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2f, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, - 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, - 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4f, 0x50, 0x58, 0xaa, 0x02, 0x0f, 0x4f, 0x73, 0x61, 0x63, - 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x10, 0x4f, 0x73, - 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0xe2, 0x02, - 0x1c, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, - 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, - 0x4f, 0x73, 0x61, 0x63, 0x3a, 0x3a, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x3a, 0x3a, 0x56, - 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x38, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x4c, 0x0a, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 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, 0x12, 0x6c, 0x61, 0x73, 0x74, 0x54, + 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x0a, + 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, + 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x07, 0x6d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x72, 0x65, + 0x61, 0x73, 0x6f, 0x6e, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x22, 0xb0, 0x01, 0x0a, 0x18, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x3d, 0x0a, + 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, + 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x53, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, + 0x65, 0x6e, 0x63, 0x65, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x12, 0x55, 0x0a, 0x0f, + 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, + 0x47, 0x72, 0x6f, 0x75, 0x70, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, + 0x6e, 0x63, 0x65, 0x52, 0x0e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x73, 0x22, 0x65, 0x0a, 0x0e, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, + 0x64, 0x65, 0x53, 0x65, 0x74, 0x12, 0x3f, 0x0a, 0x09, 0x68, 0x6f, 0x73, 0x74, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, + 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x6f, 0x73, 0x74, 0x54, + 0x79, 0x70, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x08, 0x68, 0x6f, + 0x73, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x22, 0x3b, 0x0a, 0x15, 0x43, 0x6c, + 0x75, 0x73, 0x74, 0x65, 0x72, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, + 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x70, 0x0a, 0x18, 0x43, 0x6c, 0x75, 0x73, 0x74, + 0x65, 0x72, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, + 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x22, 0x73, 0x0a, 0x1b, 0x43, 0x6c, 0x75, + 0x73, 0x74, 0x65, 0x72, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x52, + 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, + 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2a, 0xbc, + 0x01, 0x0a, 0x0c, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, + 0x1d, 0x0a, 0x19, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, + 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1d, + 0x0a, 0x19, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, + 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, 0x53, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x17, 0x0a, + 0x13, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x52, + 0x45, 0x41, 0x44, 0x59, 0x10, 0x02, 0x12, 0x18, 0x0a, 0x14, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, + 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x03, + 0x12, 0x1a, 0x0a, 0x16, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, + 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x04, 0x12, 0x1f, 0x0a, 0x1b, + 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x44, 0x45, + 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x2a, 0xd0, 0x01, + 0x0a, 0x14, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x26, 0x0a, 0x22, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, + 0x52, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, + 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x26, + 0x0a, 0x22, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, + 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, + 0x53, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x20, 0x0a, 0x1c, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, + 0x52, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, + 0x5f, 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x43, 0x4c, 0x55, 0x53, + 0x54, 0x45, 0x52, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x12, 0x23, 0x0a, 0x1f, 0x43, + 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x44, 0x45, 0x47, 0x52, 0x41, 0x44, 0x45, 0x44, 0x10, 0x04, + 0x42, 0xd5, 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x10, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, + 0x72, 0x54, 0x79, 0x70, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4c, 0x67, 0x69, + 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x70, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x69, 0x6e, 0x67, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, + 0x3b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4f, 0x50, 0x58, + 0xaa, 0x02, 0x0f, 0x4f, 0x73, 0x61, 0x63, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, + 0x56, 0x31, 0xca, 0x02, 0x10, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x5f, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1c, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x4f, 0x73, 0x61, 0x63, 0x3a, 0x3a, 0x50, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( @@ -1162,12 +1165,13 @@ var file_osac_private_v1_cluster_type_proto_goTypes = []any{ nil, // 13: osac.private.v1.ClusterSpec.NodeSetsEntry nil, // 14: osac.private.v1.ClusterStatus.NodeSetsEntry (*Metadata)(nil), // 15: osac.private.v1.Metadata - (*timestamppb.Timestamp)(nil), // 16: google.protobuf.Timestamp - (ConditionStatus)(0), // 17: osac.private.v1.ConditionStatus - (*SubnetLocalReference)(nil), // 18: osac.private.v1.SubnetLocalReference - (*SecurityGroupLocalReference)(nil), // 19: osac.private.v1.SecurityGroupLocalReference - (*HostTypeReference)(nil), // 20: osac.private.v1.HostTypeReference - (*anypb.Any)(nil), // 21: google.protobuf.Any + (*ClusterVersionReference)(nil), // 16: osac.private.v1.ClusterVersionReference + (*timestamppb.Timestamp)(nil), // 17: google.protobuf.Timestamp + (ConditionStatus)(0), // 18: osac.private.v1.ConditionStatus + (*SubnetLocalReference)(nil), // 19: osac.private.v1.SubnetLocalReference + (*SecurityGroupLocalReference)(nil), // 20: osac.private.v1.SecurityGroupLocalReference + (*HostTypeReference)(nil), // 21: osac.private.v1.HostTypeReference + (*anypb.Any)(nil), // 22: google.protobuf.Any } var file_osac_private_v1_cluster_type_proto_depIdxs = []int32{ 15, // 0: osac.private.v1.Cluster.metadata:type_name -> osac.private.v1.Metadata @@ -1176,27 +1180,28 @@ var file_osac_private_v1_cluster_type_proto_depIdxs = []int32{ 10, // 3: osac.private.v1.ClusterSpec.template:type_name -> osac.private.v1.ClusterTemplateReference 12, // 4: osac.private.v1.ClusterSpec.template_parameters:type_name -> osac.private.v1.ClusterSpec.TemplateParametersEntry 13, // 5: osac.private.v1.ClusterSpec.node_sets:type_name -> osac.private.v1.ClusterSpec.NodeSetsEntry - 4, // 6: osac.private.v1.ClusterSpec.network:type_name -> osac.private.v1.ClusterNetwork - 11, // 7: osac.private.v1.ClusterSpec.catalog_item:type_name -> osac.private.v1.ClusterCatalogItemReference - 7, // 8: osac.private.v1.ClusterSpec.network_attachment:type_name -> osac.private.v1.ClusterNetworkAttachment - 0, // 9: osac.private.v1.ClusterStatus.state:type_name -> osac.private.v1.ClusterState - 6, // 10: osac.private.v1.ClusterStatus.conditions:type_name -> osac.private.v1.ClusterCondition - 14, // 11: osac.private.v1.ClusterStatus.node_sets:type_name -> osac.private.v1.ClusterStatus.NodeSetsEntry - 16, // 12: osac.private.v1.ClusterStatus.state_transition_time:type_name -> google.protobuf.Timestamp - 1, // 13: osac.private.v1.ClusterCondition.type:type_name -> osac.private.v1.ClusterConditionType - 17, // 14: osac.private.v1.ClusterCondition.status:type_name -> osac.private.v1.ConditionStatus - 16, // 15: osac.private.v1.ClusterCondition.last_transition_time:type_name -> google.protobuf.Timestamp - 18, // 16: osac.private.v1.ClusterNetworkAttachment.subnet:type_name -> osac.private.v1.SubnetLocalReference - 19, // 17: osac.private.v1.ClusterNetworkAttachment.security_groups:type_name -> osac.private.v1.SecurityGroupLocalReference - 20, // 18: osac.private.v1.ClusterNodeSet.host_type:type_name -> osac.private.v1.HostTypeReference - 21, // 19: osac.private.v1.ClusterSpec.TemplateParametersEntry.value:type_name -> google.protobuf.Any - 8, // 20: osac.private.v1.ClusterSpec.NodeSetsEntry.value:type_name -> osac.private.v1.ClusterNodeSet - 8, // 21: osac.private.v1.ClusterStatus.NodeSetsEntry.value:type_name -> osac.private.v1.ClusterNodeSet - 22, // [22:22] is the sub-list for method output_type - 22, // [22:22] is the sub-list for method input_type - 22, // [22:22] is the sub-list for extension type_name - 22, // [22:22] is the sub-list for extension extendee - 0, // [0:22] is the sub-list for field type_name + 16, // 6: osac.private.v1.ClusterSpec.version:type_name -> osac.private.v1.ClusterVersionReference + 4, // 7: osac.private.v1.ClusterSpec.network:type_name -> osac.private.v1.ClusterNetwork + 11, // 8: osac.private.v1.ClusterSpec.catalog_item:type_name -> osac.private.v1.ClusterCatalogItemReference + 7, // 9: osac.private.v1.ClusterSpec.network_attachment:type_name -> osac.private.v1.ClusterNetworkAttachment + 0, // 10: osac.private.v1.ClusterStatus.state:type_name -> osac.private.v1.ClusterState + 6, // 11: osac.private.v1.ClusterStatus.conditions:type_name -> osac.private.v1.ClusterCondition + 14, // 12: osac.private.v1.ClusterStatus.node_sets:type_name -> osac.private.v1.ClusterStatus.NodeSetsEntry + 17, // 13: osac.private.v1.ClusterStatus.state_transition_time:type_name -> google.protobuf.Timestamp + 1, // 14: osac.private.v1.ClusterCondition.type:type_name -> osac.private.v1.ClusterConditionType + 18, // 15: osac.private.v1.ClusterCondition.status:type_name -> osac.private.v1.ConditionStatus + 17, // 16: osac.private.v1.ClusterCondition.last_transition_time:type_name -> google.protobuf.Timestamp + 19, // 17: osac.private.v1.ClusterNetworkAttachment.subnet:type_name -> osac.private.v1.SubnetLocalReference + 20, // 18: osac.private.v1.ClusterNetworkAttachment.security_groups:type_name -> osac.private.v1.SecurityGroupLocalReference + 21, // 19: osac.private.v1.ClusterNodeSet.host_type:type_name -> osac.private.v1.HostTypeReference + 22, // 20: osac.private.v1.ClusterSpec.TemplateParametersEntry.value:type_name -> google.protobuf.Any + 8, // 21: osac.private.v1.ClusterSpec.NodeSetsEntry.value:type_name -> osac.private.v1.ClusterNodeSet + 8, // 22: osac.private.v1.ClusterStatus.NodeSetsEntry.value:type_name -> osac.private.v1.ClusterNodeSet + 23, // [23:23] is the sub-list for method output_type + 23, // [23:23] is the sub-list for method input_type + 23, // [23:23] is the sub-list for extension type_name + 23, // [23:23] is the sub-list for extension extendee + 0, // [0:23] is the sub-list for field type_name } func init() { file_osac_private_v1_cluster_type_proto_init() } @@ -1204,6 +1209,7 @@ func file_osac_private_v1_cluster_type_proto_init() { if File_osac_private_v1_cluster_type_proto != nil { return } + file_osac_private_v1_cluster_version_type_proto_init() file_osac_private_v1_condition_status_type_proto_init() file_osac_private_v1_metadata_type_proto_init() file_osac_private_v1_host_type_type_proto_init() diff --git a/osac-metering/metering-service/internal/api/osac/private/v1/cluster_version_type.pb.go b/osac-metering/metering-service/internal/api/osac/private/v1/cluster_version_type.pb.go index eaa4a3514..60125db18 100644 --- a/osac-metering/metering-service/internal/api/osac/private/v1/cluster_version_type.pb.go +++ b/osac-metering/metering-service/internal/api/osac/private/v1/cluster_version_type.pb.go @@ -168,8 +168,8 @@ func (x *ClusterVersionDeprecation) GetObsolescenceTimestamp() *timestamppb.Time // Contains the details about the cluster version that are available only for the system. // // Cluster versions are admin-managed catalog entries that define which OpenShift versions are available. Users -// select a cluster version by name when creating clusters, either directly via Cluster `spec.version_name` or -// through a ClusterTemplate `spec_defaults.version_name`. Cloud Provider Admins control the version catalog +// select a cluster version when creating clusters, either directly via Cluster `spec.version` or +// through a ClusterTemplate `spec_defaults.version`. Cloud Provider Admins control the version catalog // through lifecycle-managed cluster versions. // // If `metadata.name` is omitted on creation, it is auto-generated from `spec.version` by lowercasing and @@ -274,7 +274,7 @@ type ClusterVersionSpec struct { // the previous default atomically. Auto-cleared when the version becomes OBSOLETE or disabled. // // When creating a cluster, the version is resolved with the following precedence: - // explicit `spec.version_name` > template `spec_defaults.version_name` > system default (`is_default=true`). + // explicit `spec.version` > template `spec_defaults.version` > system default (`is_default=true`). IsDefault *bool `protobuf:"varint,3,opt,name=is_default,json=isDefault,proto3,oneof" json:"is_default,omitempty"` // Current lifecycle state of the cluster version. Defaults to ACTIVE on creation if unspecified. State ClusterVersionState `protobuf:"varint,4,opt,name=state,proto3,enum=osac.private.v1.ClusterVersionState" json:"state,omitempty"` @@ -402,6 +402,75 @@ func (*ClusterVersionStatus) Descriptor() ([]byte, []int) { return file_osac_private_v1_cluster_version_type_proto_rawDescGZIP(), []int{3} } +// Reference to a ClusterVersion resource. +type ClusterVersionReference struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Project string `protobuf:"bytes,3,opt,name=project,proto3" json:"project,omitempty"` + Shared bool `protobuf:"varint,4,opt,name=shared,proto3" json:"shared,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ClusterVersionReference) Reset() { + *x = ClusterVersionReference{} + mi := &file_osac_private_v1_cluster_version_type_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ClusterVersionReference) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ClusterVersionReference) ProtoMessage() {} + +func (x *ClusterVersionReference) ProtoReflect() protoreflect.Message { + mi := &file_osac_private_v1_cluster_version_type_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ClusterVersionReference.ProtoReflect.Descriptor instead. +func (*ClusterVersionReference) Descriptor() ([]byte, []int) { + return file_osac_private_v1_cluster_version_type_proto_rawDescGZIP(), []int{4} +} + +func (x *ClusterVersionReference) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ClusterVersionReference) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ClusterVersionReference) GetProject() string { + if x != nil { + return x.Project + } + return "" +} + +func (x *ClusterVersionReference) GetShared() bool { + if x != nil { + return x.Shared + } + return false +} + var File_osac_private_v1_cluster_version_type_proto protoreflect.FileDescriptor var file_osac_private_v1_cluster_version_type_proto_rawDesc = string([]byte{ @@ -466,32 +535,40 @@ var file_osac_private_v1_cluster_version_type_proto_rawDesc = string([]byte{ 0x80, 0x02, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x64, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x69, 0x73, 0x5f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x22, 0x16, 0x0a, 0x14, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, - 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2a, 0xa8, - 0x01, 0x0a, 0x13, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x25, 0x0a, 0x21, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, + 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x6f, + 0x0a, 0x17, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, + 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, + 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2a, + 0xa8, 0x01, 0x0a, 0x13, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x25, 0x0a, 0x21, 0x43, 0x4c, 0x55, 0x53, 0x54, + 0x45, 0x52, 0x5f, 0x56, 0x45, 0x52, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, + 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x20, + 0x0a, 0x1c, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x56, 0x45, 0x52, 0x53, 0x49, 0x4f, + 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x01, + 0x12, 0x24, 0x0a, 0x20, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x56, 0x45, 0x52, 0x53, + 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x44, 0x45, 0x50, 0x52, 0x45, 0x43, + 0x41, 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x22, 0x0a, 0x1e, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x56, 0x45, 0x52, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, - 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x20, 0x0a, - 0x1c, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x56, 0x45, 0x52, 0x53, 0x49, 0x4f, 0x4e, - 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x01, 0x12, - 0x24, 0x0a, 0x20, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, 0x5f, 0x56, 0x45, 0x52, 0x53, 0x49, - 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x44, 0x45, 0x50, 0x52, 0x45, 0x43, 0x41, - 0x54, 0x45, 0x44, 0x10, 0x02, 0x12, 0x22, 0x0a, 0x1e, 0x43, 0x4c, 0x55, 0x53, 0x54, 0x45, 0x52, - 0x5f, 0x56, 0x45, 0x52, 0x53, 0x49, 0x4f, 0x4e, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x4f, - 0x42, 0x53, 0x4f, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x03, 0x42, 0xdc, 0x01, 0x0a, 0x13, 0x63, 0x6f, - 0x6d, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, - 0x31, 0x42, 0x17, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, - 0x6e, 0x54, 0x79, 0x70, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4c, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x70, 0x72, - 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x6d, 0x65, 0x74, 0x65, 0x72, - 0x69, 0x6e, 0x67, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, - 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, - 0x3b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4f, 0x50, 0x58, - 0xaa, 0x02, 0x0f, 0x4f, 0x73, 0x61, 0x63, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, - 0x56, 0x31, 0xca, 0x02, 0x10, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, - 0x65, 0x5f, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1c, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x4f, 0x73, 0x61, 0x63, 0x3a, 0x3a, 0x50, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x4f, 0x42, 0x53, 0x4f, 0x4c, 0x45, 0x54, 0x45, 0x10, 0x03, 0x42, 0xdc, 0x01, 0x0a, 0x13, 0x63, + 0x6f, 0x6d, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, + 0x76, 0x31, 0x42, 0x17, 0x43, 0x6c, 0x75, 0x73, 0x74, 0x65, 0x72, 0x56, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4c, 0x67, + 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x70, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x6d, 0x65, 0x74, 0x65, + 0x72, 0x69, 0x6e, 0x67, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, + 0x69, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, + 0x31, 0x3b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4f, 0x50, + 0x58, 0xaa, 0x02, 0x0f, 0x4f, 0x73, 0x61, 0x63, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x2e, 0x56, 0x31, 0xca, 0x02, 0x10, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1c, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x4f, 0x73, 0x61, 0x63, 0x3a, 0x3a, 0x50, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, }) var ( @@ -507,20 +584,21 @@ func file_osac_private_v1_cluster_version_type_proto_rawDescGZIP() []byte { } var file_osac_private_v1_cluster_version_type_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_osac_private_v1_cluster_version_type_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_osac_private_v1_cluster_version_type_proto_msgTypes = make([]protoimpl.MessageInfo, 5) var file_osac_private_v1_cluster_version_type_proto_goTypes = []any{ (ClusterVersionState)(0), // 0: osac.private.v1.ClusterVersionState (*ClusterVersionDeprecation)(nil), // 1: osac.private.v1.ClusterVersionDeprecation (*ClusterVersion)(nil), // 2: osac.private.v1.ClusterVersion (*ClusterVersionSpec)(nil), // 3: osac.private.v1.ClusterVersionSpec (*ClusterVersionStatus)(nil), // 4: osac.private.v1.ClusterVersionStatus - (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp - (*Metadata)(nil), // 6: osac.private.v1.Metadata + (*ClusterVersionReference)(nil), // 5: osac.private.v1.ClusterVersionReference + (*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp + (*Metadata)(nil), // 7: osac.private.v1.Metadata } var file_osac_private_v1_cluster_version_type_proto_depIdxs = []int32{ - 5, // 0: osac.private.v1.ClusterVersionDeprecation.deprecation_timestamp:type_name -> google.protobuf.Timestamp - 5, // 1: osac.private.v1.ClusterVersionDeprecation.obsolescence_timestamp:type_name -> google.protobuf.Timestamp - 6, // 2: osac.private.v1.ClusterVersion.metadata:type_name -> osac.private.v1.Metadata + 6, // 0: osac.private.v1.ClusterVersionDeprecation.deprecation_timestamp:type_name -> google.protobuf.Timestamp + 6, // 1: osac.private.v1.ClusterVersionDeprecation.obsolescence_timestamp:type_name -> google.protobuf.Timestamp + 7, // 2: osac.private.v1.ClusterVersion.metadata:type_name -> osac.private.v1.Metadata 3, // 3: osac.private.v1.ClusterVersion.spec:type_name -> osac.private.v1.ClusterVersionSpec 4, // 4: osac.private.v1.ClusterVersion.status:type_name -> osac.private.v1.ClusterVersionStatus 0, // 5: osac.private.v1.ClusterVersionSpec.state:type_name -> osac.private.v1.ClusterVersionState @@ -545,7 +623,7 @@ func file_osac_private_v1_cluster_version_type_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_osac_private_v1_cluster_version_type_proto_rawDesc), len(file_osac_private_v1_cluster_version_type_proto_rawDesc)), NumEnums: 1, - NumMessages: 4, + NumMessages: 5, NumExtensions: 0, NumServices: 0, }, diff --git a/osac-metering/metering-service/internal/api/osac/private/v1/compute_instance_type.pb.go b/osac-metering/metering-service/internal/api/osac/private/v1/compute_instance_type.pb.go index 435b72a12..975074b04 100644 --- a/osac-metering/metering-service/internal/api/osac/private/v1/compute_instance_type.pb.go +++ b/osac-metering/metering-service/internal/api/osac/private/v1/compute_instance_type.pb.go @@ -20,6 +20,7 @@ package privatev1 import ( + _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" anypb "google.golang.org/protobuf/types/known/anypb" @@ -415,9 +416,13 @@ type ComputeInstanceSpec struct { // Reference to an instance type. Specifies the compute configuration (cores, memory) // for this instance. The API validates that the instance type exists and is not OBSOLETE; // resolution of cores/memory_gib happens in the reconciler. - InstanceType *InstanceTypeReference `protobuf:"bytes,17,opt,name=instance_type,json=instanceType,proto3" json:"instance_type,omitempty"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + InstanceType *InstanceTypeReference `protobuf:"bytes,17,opt,name=instance_type,json=instanceType,proto3" json:"instance_type,omitempty"` + // When true, the system auto-selects an ExternalIPPool and creates an ExternalIP + // with an ExternalIPAttachment for this instance atomically during creation. + // Immutable after creation. + AutoExternalIpAttachment bool `protobuf:"varint,18,opt,name=auto_external_ip_attachment,json=autoExternalIpAttachment,proto3" json:"auto_external_ip_attachment,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ComputeInstanceSpec) Reset() { @@ -541,6 +546,13 @@ func (x *ComputeInstanceSpec) GetInstanceType() *InstanceTypeReference { return nil } +func (x *ComputeInstanceSpec) GetAutoExternalIpAttachment() bool { + if x != nil { + return x.AutoExternalIpAttachment + } + return false +} + type ComputeInstanceStatus struct { state protoimpl.MessageState `protogen:"open.v1"` // Public fields. @@ -915,260 +927,266 @@ var file_osac_private_v1_compute_instance_type_proto_rawDesc = string([]byte{ 0x0a, 0x2b, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x5f, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0f, 0x6f, - 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x19, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, - 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 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, 0x2b, 0x6f, 0x73, 0x61, 0x63, - 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x64, - 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5f, 0x74, 0x79, 0x70, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x23, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x28, 0x6f, 0x73, - 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x69, 0x6e, - 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, - 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x29, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, - 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x1a, 0x21, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, - 0x76, 0x31, 0x2f, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xd2, 0x01, 0x0a, 0x0f, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, - 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x35, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6f, 0x73, 0x61, - 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, - 0x38, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, - 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, - 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x12, 0x3e, 0x0a, 0x06, 0x73, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6f, 0x73, 0x61, 0x63, - 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, - 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x56, 0x0a, 0x14, 0x43, 0x6f, 0x6d, - 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x6d, 0x61, 0x67, - 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, 0x79, - 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x66, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x65, - 0x66, 0x22, 0x30, 0x0a, 0x13, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, - 0x61, 0x6e, 0x63, 0x65, 0x44, 0x69, 0x73, 0x6b, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x69, 0x7a, 0x65, - 0x5f, 0x67, 0x69, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x73, 0x69, 0x7a, 0x65, - 0x47, 0x69, 0x62, 0x22, 0xa9, 0x01, 0x0a, 0x11, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, - 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x3d, 0x0a, 0x06, 0x73, 0x75, 0x62, - 0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6f, 0x73, 0x61, 0x63, - 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x62, 0x6e, - 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, - 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x12, 0x55, 0x0a, 0x0f, 0x73, 0x65, 0x63, 0x75, - 0x72, 0x69, 0x74, 0x79, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x2c, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x47, 0x72, 0x6f, 0x75, - 0x70, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, - 0x0e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x22, - 0xa8, 0x09, 0x0a, 0x13, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, - 0x6e, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x4d, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x6f, 0x73, 0x61, 0x63, + 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1f, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, + 0x5f, 0x62, 0x65, 0x68, 0x61, 0x76, 0x69, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, + 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 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, 0x2b, 0x6f, 0x73, 0x61, + 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x23, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, + 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x28, 0x6f, + 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x69, + 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x5f, 0x74, 0x79, 0x70, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x29, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, + 0x79, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x1a, 0x21, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xd2, 0x01, 0x0a, 0x0f, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, + 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x35, 0x0a, 0x08, 0x6d, 0x65, 0x74, + 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6f, 0x73, + 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, + 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x12, 0x38, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, + 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, + 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x12, 0x3e, 0x0a, 0x06, 0x73, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6f, 0x73, 0x61, + 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, + 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, + 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x56, 0x0a, 0x14, 0x43, 0x6f, + 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x6d, 0x61, + 0x67, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x54, + 0x79, 0x70, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x72, 0x65, + 0x66, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, + 0x65, 0x66, 0x22, 0x30, 0x0a, 0x13, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, + 0x74, 0x61, 0x6e, 0x63, 0x65, 0x44, 0x69, 0x73, 0x6b, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x69, 0x7a, + 0x65, 0x5f, 0x67, 0x69, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x73, 0x69, 0x7a, + 0x65, 0x47, 0x69, 0x62, 0x22, 0xa9, 0x01, 0x0a, 0x11, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x3d, 0x0a, 0x06, 0x73, 0x75, + 0x62, 0x6e, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6f, 0x73, 0x61, + 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x75, 0x62, + 0x6e, 0x65, 0x74, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, + 0x65, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x12, 0x55, 0x0a, 0x0f, 0x73, 0x65, 0x63, + 0x75, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x18, 0x02, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x2c, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x47, 0x72, 0x6f, + 0x75, 0x70, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, + 0x52, 0x0e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x47, 0x72, 0x6f, 0x75, 0x70, 0x73, + 0x22, 0xec, 0x09, 0x0a, 0x13, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, + 0x61, 0x6e, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x12, 0x4d, 0x0a, 0x08, 0x74, 0x65, 0x6d, 0x70, + 0x6c, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x6f, 0x73, 0x61, + 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, + 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x54, 0x65, 0x6d, 0x70, + 0x6c, 0x61, 0x74, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x08, 0x74, + 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x6d, 0x0a, 0x13, 0x74, 0x65, 0x6d, 0x70, 0x6c, + 0x61, 0x74, 0x65, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, + 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, + 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x52, 0x12, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, + 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x51, 0x0a, 0x14, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, + 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, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x65, 0x64, 0x41, 0x74, 0x88, 0x01, 0x01, 0x12, 0x40, 0x0a, 0x05, 0x69, 0x6d, 0x61, + 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, + 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x75, + 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x48, + 0x01, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x88, 0x01, 0x01, 0x12, 0x29, 0x0a, 0x0e, 0x73, + 0x73, 0x68, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x0c, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x4b, 0x65, 0x79, 0x88, 0x01, 0x01, 0x12, 0x46, 0x0a, 0x09, 0x62, 0x6f, 0x6f, 0x74, 0x5f, 0x64, + 0x69, 0x73, 0x6b, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, - 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x54, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x08, 0x74, 0x65, - 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x12, 0x6d, 0x0a, 0x13, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, - 0x74, 0x65, 0x5f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x3c, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, - 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x70, 0x65, 0x63, 0x2e, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, - 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x52, 0x12, 0x74, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, - 0x65, 0x74, 0x65, 0x72, 0x73, 0x12, 0x51, 0x0a, 0x14, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, - 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x03, 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, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x65, 0x64, 0x41, 0x74, 0x88, 0x01, 0x01, 0x12, 0x40, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, - 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, - 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, - 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x6d, 0x61, 0x67, 0x65, 0x48, 0x01, - 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x88, 0x01, 0x01, 0x12, 0x29, 0x0a, 0x0e, 0x73, 0x73, - 0x68, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x48, 0x02, 0x52, 0x0c, 0x73, 0x73, 0x68, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, - 0x65, 0x79, 0x88, 0x01, 0x01, 0x12, 0x46, 0x0a, 0x09, 0x62, 0x6f, 0x6f, 0x74, 0x5f, 0x64, 0x69, - 0x73, 0x6b, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, + 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x44, 0x69, 0x73, 0x6b, 0x48, + 0x03, 0x52, 0x08, 0x62, 0x6f, 0x6f, 0x74, 0x44, 0x69, 0x73, 0x6b, 0x88, 0x01, 0x01, 0x12, 0x4f, + 0x0a, 0x10, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73, + 0x6b, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x75, - 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x44, 0x69, 0x73, 0x6b, 0x48, 0x03, - 0x52, 0x08, 0x62, 0x6f, 0x6f, 0x74, 0x44, 0x69, 0x73, 0x6b, 0x88, 0x01, 0x01, 0x12, 0x4f, 0x0a, - 0x10, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x69, 0x73, 0x6b, - 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, - 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, - 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x44, 0x69, 0x73, 0x6b, 0x52, 0x0f, 0x61, - 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x44, 0x69, 0x73, 0x6b, 0x73, 0x12, 0x26, - 0x0a, 0x0c, 0x72, 0x75, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x0a, - 0x20, 0x01, 0x28, 0x09, 0x48, 0x04, 0x52, 0x0b, 0x72, 0x75, 0x6e, 0x53, 0x74, 0x72, 0x61, 0x74, - 0x65, 0x67, 0x79, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x48, 0x05, 0x52, 0x08, 0x75, 0x73, 0x65, - 0x72, 0x44, 0x61, 0x74, 0x61, 0x88, 0x01, 0x01, 0x12, 0x53, 0x0a, 0x13, 0x6e, 0x65, 0x74, 0x77, - 0x6f, 0x72, 0x6b, 0x5f, 0x61, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, - 0x0e, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x41, - 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x12, 0x6e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x57, 0x0a, - 0x0c, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x0f, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, - 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x49, 0x74, 0x65, 0x6d, - 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0b, 0x63, 0x61, 0x74, 0x61, 0x6c, - 0x6f, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x22, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x77, 0x69, 0x6e, - 0x64, 0x6f, 0x77, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, 0x48, 0x06, 0x52, 0x09, 0x69, 0x73, - 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x88, 0x01, 0x01, 0x12, 0x4b, 0x0a, 0x0d, 0x69, 0x6e, - 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x11, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x26, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, - 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0c, 0x69, 0x6e, 0x73, 0x74, 0x61, - 0x6e, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x1a, 0x5b, 0x0a, 0x17, 0x54, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x73, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x3a, 0x02, 0x38, 0x01, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, - 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x42, 0x08, 0x0a, - 0x06, 0x5f, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x73, 0x73, 0x68, 0x5f, - 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x62, - 0x6f, 0x6f, 0x74, 0x5f, 0x64, 0x69, 0x73, 0x6b, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x72, 0x75, 0x6e, - 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x75, 0x73, - 0x65, 0x72, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x69, 0x73, 0x5f, 0x77, - 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x4a, 0x04, 0x08, 0x05, 0x10, 0x06, 0x4a, 0x04, 0x08, 0x06, - 0x10, 0x07, 0x4a, 0x04, 0x08, 0x0c, 0x10, 0x0d, 0x4a, 0x04, 0x08, 0x0d, 0x10, 0x0e, 0x52, 0x05, - 0x63, 0x6f, 0x72, 0x65, 0x73, 0x52, 0x0a, 0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x5f, 0x67, 0x69, - 0x62, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x52, 0x0f, 0x73, 0x65, 0x63, 0x75, 0x72, - 0x69, 0x74, 0x79, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x22, 0xe3, 0x03, 0x0a, 0x15, 0x43, - 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x74, - 0x61, 0x74, 0x75, 0x73, 0x12, 0x3b, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x25, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, - 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, - 0x65, 0x12, 0x49, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, - 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x13, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, - 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x49, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, - 0x68, 0x75, 0x62, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x68, 0x75, 0x62, 0x12, 0x4b, - 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, - 0x5f, 0x61, 0x74, 0x18, 0x05, 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, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x52, 0x65, 0x73, - 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x41, 0x74, 0x88, 0x01, 0x01, 0x12, 0x2e, 0x0a, 0x13, 0x65, - 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x69, 0x70, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, - 0x73, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x49, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x53, 0x0a, 0x15, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, - 0x74, 0x69, 0x6d, 0x65, 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, 0x48, 0x01, 0x52, 0x13, 0x73, 0x74, 0x61, 0x74, 0x65, 0x54, - 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x88, 0x01, 0x01, - 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, - 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x42, 0x18, 0x0a, 0x16, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, - 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, - 0x22, 0xb8, 0x02, 0x0a, 0x18, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, - 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x41, 0x0a, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2d, 0x2e, 0x6f, 0x73, - 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, - 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6e, - 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, - 0x12, 0x38, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x20, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, - 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, - 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x4c, 0x0a, 0x14, 0x6c, 0x61, - 0x73, 0x74, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, - 0x6d, 0x65, 0x18, 0x03, 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, 0x12, 0x6c, 0x61, 0x73, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, - 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, - 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, - 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x42, - 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0x43, 0x0a, 0x1d, 0x43, - 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x4c, 0x6f, - 0x63, 0x61, 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x22, 0x78, 0x0a, 0x20, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, - 0x6e, 0x63, 0x65, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, - 0x65, 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, - 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, - 0x63, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x22, 0x7b, 0x0a, 0x23, 0x43, 0x6f, - 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x61, 0x74, - 0x61, 0x6c, 0x6f, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, + 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x44, 0x69, 0x73, 0x6b, 0x52, 0x0f, + 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x44, 0x69, 0x73, 0x6b, 0x73, 0x12, + 0x26, 0x0a, 0x0c, 0x72, 0x75, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x09, 0x48, 0x04, 0x52, 0x0b, 0x72, 0x75, 0x6e, 0x53, 0x74, 0x72, 0x61, + 0x74, 0x65, 0x67, 0x79, 0x88, 0x01, 0x01, 0x12, 0x20, 0x0a, 0x09, 0x75, 0x73, 0x65, 0x72, 0x5f, + 0x64, 0x61, 0x74, 0x61, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x48, 0x05, 0x52, 0x08, 0x75, 0x73, + 0x65, 0x72, 0x44, 0x61, 0x74, 0x61, 0x88, 0x01, 0x01, 0x12, 0x53, 0x0a, 0x13, 0x6e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x61, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x73, + 0x18, 0x0e, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x12, 0x6e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x57, + 0x0a, 0x0c, 0x63, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x0f, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, + 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x49, 0x74, 0x65, + 0x6d, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0b, 0x63, 0x61, 0x74, 0x61, + 0x6c, 0x6f, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x22, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x77, 0x69, + 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, 0x08, 0x48, 0x06, 0x52, 0x09, 0x69, + 0x73, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x88, 0x01, 0x01, 0x12, 0x4b, 0x0a, 0x0d, 0x69, + 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x11, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x54, 0x79, 0x70, + 0x65, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x52, 0x0c, 0x69, 0x6e, 0x73, 0x74, + 0x61, 0x6e, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x42, 0x0a, 0x1b, 0x61, 0x75, 0x74, 0x6f, + 0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x69, 0x70, 0x5f, 0x61, 0x74, 0x74, + 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x08, 0x42, 0x03, 0xe0, + 0x41, 0x05, 0x52, 0x18, 0x61, 0x75, 0x74, 0x6f, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x49, 0x70, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x1a, 0x5b, 0x0a, 0x17, + 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, + 0x72, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x2a, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x17, 0x0a, 0x15, 0x5f, 0x72, 0x65, + 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x65, 0x64, 0x5f, + 0x61, 0x74, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x42, 0x11, 0x0a, 0x0f, + 0x5f, 0x73, 0x73, 0x68, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x42, + 0x0c, 0x0a, 0x0a, 0x5f, 0x62, 0x6f, 0x6f, 0x74, 0x5f, 0x64, 0x69, 0x73, 0x6b, 0x42, 0x0f, 0x0a, + 0x0d, 0x5f, 0x72, 0x75, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x42, 0x0c, + 0x0a, 0x0a, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x42, 0x0d, 0x0a, 0x0b, + 0x5f, 0x69, 0x73, 0x5f, 0x77, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x73, 0x4a, 0x04, 0x08, 0x05, 0x10, + 0x06, 0x4a, 0x04, 0x08, 0x06, 0x10, 0x07, 0x4a, 0x04, 0x08, 0x0c, 0x10, 0x0d, 0x4a, 0x04, 0x08, + 0x0d, 0x10, 0x0e, 0x52, 0x05, 0x63, 0x6f, 0x72, 0x65, 0x73, 0x52, 0x0a, 0x6d, 0x65, 0x6d, 0x6f, + 0x72, 0x79, 0x5f, 0x67, 0x69, 0x62, 0x52, 0x06, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x52, 0x0f, + 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x73, 0x22, + 0xe3, 0x03, 0x0a, 0x15, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, + 0x6e, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x3b, 0x0a, 0x05, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x25, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, + 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x75, + 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, + 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x49, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6f, 0x73, 0x61, + 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, + 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x64, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x12, 0x2e, 0x0a, 0x13, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x69, 0x70, + 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x49, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, + 0x73, 0x12, 0x10, 0x0a, 0x03, 0x68, 0x75, 0x62, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x68, 0x75, 0x62, 0x12, 0x4b, 0x0a, 0x11, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x72, 0x65, 0x73, 0x74, + 0x61, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x05, 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, 0x0f, 0x6c, 0x61, + 0x73, 0x74, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x41, 0x74, 0x88, 0x01, 0x01, + 0x12, 0x2e, 0x0a, 0x13, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x69, 0x70, 0x5f, + 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x65, + 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x49, 0x70, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, + 0x12, 0x53, 0x0a, 0x15, 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 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, 0x48, 0x01, 0x52, 0x13, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, + 0x6d, 0x65, 0x88, 0x01, 0x01, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x72, + 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x42, 0x18, 0x0a, 0x16, 0x5f, + 0x73, 0x74, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x22, 0xb8, 0x02, 0x0a, 0x18, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, + 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x41, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x2d, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, + 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x38, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, + 0x4c, 0x0a, 0x14, 0x6c, 0x61, 0x73, 0x74, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 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, 0x12, 0x6c, 0x61, 0x73, 0x74, 0x54, + 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x1b, 0x0a, + 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, + 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x6d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x07, 0x6d, + 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x72, 0x65, + 0x61, 0x73, 0x6f, 0x6e, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x22, 0x43, 0x0a, 0x1d, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, + 0x6e, 0x63, 0x65, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, - 0x16, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2a, 0xbb, 0x02, 0x0a, 0x14, 0x43, 0x6f, 0x6d, 0x70, - 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, - 0x12, 0x26, 0x0a, 0x22, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, - 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, - 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x23, 0x0a, 0x1f, 0x43, 0x4f, 0x4d, 0x50, - 0x55, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, - 0x54, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x22, 0x0a, - 0x1e, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, - 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x52, 0x55, 0x4e, 0x4e, 0x49, 0x4e, 0x47, 0x10, - 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x53, - 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, - 0x45, 0x44, 0x10, 0x03, 0x12, 0x23, 0x0a, 0x1f, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, - 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x44, - 0x45, 0x4c, 0x45, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x04, 0x12, 0x23, 0x0a, 0x1f, 0x43, 0x4f, 0x4d, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x78, 0x0a, 0x20, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, + 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x54, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x74, 0x65, + 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, + 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, + 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, + 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x22, + 0x7b, 0x0a, 0x23, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, + 0x63, 0x65, 0x43, 0x61, 0x74, 0x61, 0x6c, 0x6f, 0x67, 0x49, 0x74, 0x65, 0x6d, 0x52, 0x65, 0x66, + 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x2a, 0xbb, 0x02, 0x0a, + 0x14, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x26, 0x0a, 0x22, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, + 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x23, 0x0a, + 0x1f, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, + 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x49, 0x4e, 0x47, + 0x10, 0x01, 0x12, 0x22, 0x0a, 0x1e, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x49, 0x4e, + 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x52, 0x55, 0x4e, + 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x21, 0x0a, 0x1d, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, + 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, + 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x12, 0x23, 0x0a, 0x1f, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, - 0x41, 0x54, 0x45, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x49, 0x4e, 0x47, 0x10, 0x05, 0x12, 0x22, - 0x0a, 0x1e, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, - 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x45, 0x44, - 0x10, 0x06, 0x12, 0x21, 0x0a, 0x1d, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x49, 0x4e, - 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x50, 0x41, 0x55, - 0x53, 0x45, 0x44, 0x10, 0x07, 0x2a, 0x89, 0x03, 0x0a, 0x1c, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, - 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2f, 0x0a, 0x2b, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, - 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, - 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, - 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x39, 0x0a, 0x35, 0x43, 0x4f, 0x4d, 0x50, 0x55, + 0x41, 0x54, 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x04, 0x12, 0x23, + 0x0a, 0x1f, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, + 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x49, 0x4e, + 0x47, 0x10, 0x05, 0x12, 0x22, 0x0a, 0x1e, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x49, + 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x53, 0x54, + 0x4f, 0x50, 0x50, 0x45, 0x44, 0x10, 0x06, 0x12, 0x21, 0x0a, 0x1d, 0x43, 0x4f, 0x4d, 0x50, 0x55, + 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x53, 0x54, 0x41, 0x54, + 0x45, 0x5f, 0x50, 0x41, 0x55, 0x53, 0x45, 0x44, 0x10, 0x07, 0x2a, 0x89, 0x03, 0x0a, 0x1c, 0x43, + 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, + 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2f, 0x0a, 0x2b, 0x43, + 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, + 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x39, 0x0a, 0x35, + 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, + 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, + 0x43, 0x4f, 0x4e, 0x46, 0x49, 0x47, 0x55, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x50, + 0x50, 0x4c, 0x49, 0x45, 0x44, 0x10, 0x01, 0x12, 0x29, 0x0a, 0x25, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x44, - 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x46, 0x49, - 0x47, 0x55, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x41, 0x50, 0x50, 0x4c, 0x49, 0x45, 0x44, - 0x10, 0x01, 0x12, 0x29, 0x0a, 0x25, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x49, 0x4e, + 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x59, + 0x10, 0x02, 0x12, 0x37, 0x0a, 0x33, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, - 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, 0x02, 0x12, 0x37, 0x0a, - 0x33, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, - 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, - 0x5f, 0x52, 0x45, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x49, 0x4e, 0x5f, 0x50, 0x52, 0x4f, 0x47, - 0x52, 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x32, 0x0a, 0x2e, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, - 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, - 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x53, 0x54, 0x41, 0x52, - 0x54, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x04, 0x12, 0x2f, 0x0a, 0x2b, 0x43, 0x4f, - 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x43, - 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x50, 0x52, - 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x45, 0x44, 0x10, 0x05, 0x12, 0x34, 0x0a, 0x30, 0x43, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x49, 0x4e, + 0x5f, 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x03, 0x12, 0x32, 0x0a, 0x2e, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, - 0x45, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x52, 0x45, 0x51, 0x55, 0x49, 0x52, 0x45, 0x44, 0x10, - 0x06, 0x42, 0xdd, 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, - 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x18, 0x43, 0x6f, 0x6d, 0x70, 0x75, - 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x50, 0x72, - 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, - 0x6d, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x6f, - 0x73, 0x61, 0x63, 0x2d, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2f, 0x69, 0x6e, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, - 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, - 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4f, 0x50, 0x58, 0xaa, 0x02, 0x0f, 0x4f, 0x73, 0x61, 0x63, - 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x10, 0x4f, 0x73, - 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0xe2, 0x02, - 0x1c, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, - 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, - 0x4f, 0x73, 0x61, 0x63, 0x3a, 0x3a, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x3a, 0x3a, 0x56, - 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x45, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x04, 0x12, + 0x2f, 0x0a, 0x2b, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, 0x41, + 0x4e, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x56, 0x49, 0x53, 0x49, 0x4f, 0x4e, 0x45, 0x44, 0x10, 0x05, + 0x12, 0x34, 0x0a, 0x30, 0x43, 0x4f, 0x4d, 0x50, 0x55, 0x54, 0x45, 0x5f, 0x49, 0x4e, 0x53, 0x54, + 0x41, 0x4e, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, + 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x52, 0x45, 0x51, 0x55, + 0x49, 0x52, 0x45, 0x44, 0x10, 0x06, 0x42, 0xdd, 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x6f, + 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x18, + 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x54, + 0x79, 0x70, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4c, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x70, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x69, 0x6e, + 0x67, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6f, + 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x70, + 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4f, 0x50, 0x58, 0xaa, 0x02, + 0x0f, 0x4f, 0x73, 0x61, 0x63, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x56, 0x31, + 0xca, 0x02, 0x10, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, + 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1c, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0xea, 0x02, 0x11, 0x4f, 0x73, 0x61, 0x63, 0x3a, 0x3a, 0x50, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( diff --git a/osac-metering/metering-service/internal/api/osac/private/v1/metadata_type.pb.go b/osac-metering/metering-service/internal/api/osac/private/v1/metadata_type.pb.go index a5cd29aa4..498c63cc4 100644 --- a/osac-metering/metering-service/internal/api/osac/private/v1/metadata_type.pb.go +++ b/osac-metering/metering-service/internal/api/osac/private/v1/metadata_type.pb.go @@ -50,15 +50,13 @@ type Metadata struct { Creator string `protobuf:"bytes,4,opt,name=creator,proto3" json:"creator,omitempty"` // Tenant contains the identifier of the tenant that the object belongs to. Tenant string `protobuf:"bytes,5,opt,name=tenant,proto3" json:"tenant,omitempty"` - // Human friendly name of the object. + // Mandatory, immutable, and unique within scope. Must be a valid RFC 1123 DNS label. // - // Has the same restrictions than DNS labels, as described in RFC 1035: + // Has the same restrictions as DNS labels, as described in RFC 1123: // // - Must be between 1 and 63 characters long. - // - Must only contain letters (a-z), digits (0-9) and hyphens (-). - // - It isn't case sensitive. - // - // It is optional and not unique, so multiple objecs, even created by the same user or tenant, can have the same name. + // - Must only contain lowercase letters (a-z), digits (0-9) and hyphens (-). + // - Must start and end with an alphanumeric character. Name string `protobuf:"bytes,6,opt,name=name,proto3" json:"name,omitempty"` // Labels contains key-value pairs for organizing and selecting objects. // @@ -212,7 +210,7 @@ var file_osac_private_v1_metadata_type_proto_rawDesc = string([]byte{ 0x64, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 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, 0x22, 0xf4, 0x04, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf3, 0x04, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x49, 0x0a, 0x12, 0x63, 0x72, 0x65, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 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, @@ -227,45 +225,45 @@ var file_osac_private_v1_metadata_type_proto_rawDesc = string([]byte{ 0x61, 0x6c, 0x69, 0x7a, 0x65, 0x72, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x72, 0x65, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x12, 0x44, 0x0a, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x30, 0xba, 0x48, 0x2d, 0x72, 0x2b, 0x18, 0x3f, - 0x32, 0x27, 0x5e, 0x28, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x5b, 0x61, 0x2d, - 0x7a, 0x30, 0x2d, 0x39, 0x2d, 0x5d, 0x7b, 0x30, 0x2c, 0x36, 0x31, 0x7d, 0x5b, 0x61, 0x2d, 0x7a, - 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x29, 0x3f, 0x24, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x3d, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x25, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, - 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x4c, - 0x0a, 0x0b, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x08, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x41, - 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, - 0x0b, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, - 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, - 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, - 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, - 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, 0x1a, 0x3e, 0x0a, 0x10, 0x41, - 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 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, 0xd6, 0x01, 0x0a, 0x13, - 0x63, 0x6f, 0x6d, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x2e, 0x76, 0x31, 0x42, 0x11, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, - 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, - 0x74, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2f, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6f, 0x73, 0x61, - 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x70, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4f, 0x50, 0x58, 0xaa, 0x02, 0x0f, 0x4f, - 0x73, 0x61, 0x63, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, - 0x10, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, - 0x31, 0xe2, 0x02, 0x1c, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x5f, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0xea, 0x02, 0x11, 0x4f, 0x73, 0x61, 0x63, 0x3a, 0x3a, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x09, 0x52, 0x06, 0x74, 0x65, 0x6e, 0x61, 0x6e, 0x74, 0x12, 0x43, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x2f, 0xba, 0x48, 0x2c, 0x72, 0x2a, 0x10, 0x01, + 0x18, 0x3f, 0x32, 0x24, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x28, 0x5b, 0x61, + 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x2d, 0x5d, 0x7b, 0x30, 0x2c, 0x36, 0x31, 0x7d, 0x5b, 0x61, 0x2d, + 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x3d, + 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, + 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, + 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x12, 0x4c, 0x0a, + 0x0b, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x08, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x41, 0x6e, + 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, + 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x05, 0x52, 0x07, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 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, 0x1a, 0x3e, 0x0a, 0x10, 0x41, 0x6e, + 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 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, 0xd6, 0x01, 0x0a, 0x13, 0x63, + 0x6f, 0x6d, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, + 0x76, 0x31, 0x42, 0x11, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, 0x65, + 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, + 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2f, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6f, 0x73, 0x61, 0x63, + 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x70, 0x72, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4f, 0x50, 0x58, 0xaa, 0x02, 0x0f, 0x4f, 0x73, + 0x61, 0x63, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x10, + 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, + 0xe2, 0x02, 0x1c, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, + 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, + 0x02, 0x11, 0x4f, 0x73, 0x61, 0x63, 0x3a, 0x3a, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x3a, + 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( diff --git a/osac-metering/metering-service/internal/api/osac/private/v1/network_class_type.pb.go b/osac-metering/metering-service/internal/api/osac/private/v1/network_class_type.pb.go index 52e4be6a4..ec730aa85 100644 --- a/osac-metering/metering-service/internal/api/osac/private/v1/network_class_type.pb.go +++ b/osac-metering/metering-service/internal/api/osac/private/v1/network_class_type.pb.go @@ -137,11 +137,13 @@ type NetworkClass struct { IsDefault *bool `protobuf:"varint,9,opt,name=is_default,json=isDefault,proto3,oneof" json:"is_default,omitempty"` // Identifier of the fabric manager that handles physical network operations for this class. The fabric manager // controls all fabric-level operations: virtual network segments, security groups, external IPs, and NAT gateways. - // For example: "netris", "neutron". - FabricManager string `protobuf:"bytes,10,opt,name=fabric_manager,json=fabricManager,proto3" json:"fabric_manager,omitempty"` + // For example: "netris", "neutron". Not needed for k8s-only deployments — if unset, k8s_manager must be set + // instead, and k8s-only operations (VirtualNetwork, Subnet, SecurityGroup, ExternalIP) are routed to it. + FabricManager *string `protobuf:"bytes,10,opt,name=fabric_manager,json=fabricManager,proto3,oneof" json:"fabric_manager,omitempty"` // Identifier of the K8s manager that bridges the Kubernetes overlay to the physical fabric. Only needed for regions // that host VMs — the K8s manager creates the overlay network on hosting clusters and bridges it to the fabric - // segment. For example: "cudn_localnet". If not set, the region does not support VM workloads. + // segment. For example: "cudn_localnet". If not set, the region does not support VM workloads. At least one of + // fabric_manager or k8s_manager must be set. K8SManager *string `protobuf:"bytes,11,opt,name=k8s_manager,json=k8sManager,proto3,oneof" json:"k8s_manager,omitempty"` // Desired configuration for this network class. Spec *NetworkClassSpec `protobuf:"bytes,12,opt,name=spec,proto3" json:"spec,omitempty"` @@ -243,8 +245,8 @@ func (x *NetworkClass) GetIsDefault() bool { } func (x *NetworkClass) GetFabricManager() string { - if x != nil { - return x.FabricManager + if x != nil && x.FabricManager != nil { + return *x.FabricManager } return "" } @@ -639,7 +641,7 @@ var file_osac_private_v1_network_class_type_proto_rawDesc = string([]byte{ 0x74, 0x61, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x29, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x73, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x5f, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x5f, 0x74, 0x79, - 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xe5, 0x04, 0x0a, 0x0c, 0x4e, 0x65, 0x74, + 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xfd, 0x04, 0x0a, 0x0c, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x35, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6f, 0x73, @@ -667,158 +669,160 @@ var file_osac_private_v1_network_class_type_proto_rawDesc = string([]byte{ 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x22, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, - 0x09, 0x69, 0x73, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x88, 0x01, 0x01, 0x12, 0x25, 0x0a, + 0x09, 0x69, 0x73, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x88, 0x01, 0x01, 0x12, 0x2a, 0x0a, 0x0e, 0x66, 0x61, 0x62, 0x72, 0x69, 0x63, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x18, - 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x66, 0x61, 0x62, 0x72, 0x69, 0x63, 0x4d, 0x61, 0x6e, - 0x61, 0x67, 0x65, 0x72, 0x12, 0x24, 0x0a, 0x0b, 0x6b, 0x38, 0x73, 0x5f, 0x6d, 0x61, 0x6e, 0x61, - 0x67, 0x65, 0x72, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0a, 0x6b, 0x38, 0x73, - 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x35, 0x0a, 0x04, 0x73, 0x70, - 0x65, 0x63, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, - 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x53, 0x70, 0x65, 0x63, 0x52, 0x04, 0x73, 0x70, 0x65, - 0x63, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x69, 0x73, 0x5f, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, - 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x6b, 0x38, 0x73, 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, - 0x22, 0x81, 0x02, 0x0a, 0x10, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6c, 0x61, 0x73, - 0x73, 0x53, 0x70, 0x65, 0x63, 0x12, 0x3c, 0x0a, 0x08, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, - 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, - 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, 0x52, 0x08, 0x64, 0x65, 0x66, 0x61, 0x75, - 0x6c, 0x74, 0x73, 0x12, 0x5c, 0x0a, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, - 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x29, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6c, 0x61, 0x73, 0x73, - 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x52, 0x13, 0x64, 0x69, - 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, - 0x73, 0x12, 0x3b, 0x0a, 0x11, 0x76, 0x69, 0x70, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x5f, - 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x42, 0x0a, 0xba, 0x48, - 0x07, 0x1a, 0x05, 0x18, 0x80, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x0f, 0x76, 0x69, 0x70, 0x50, - 0x72, 0x65, 0x66, 0x69, 0x78, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x88, 0x01, 0x01, 0x42, 0x14, - 0x0a, 0x12, 0x5f, 0x76, 0x69, 0x70, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x5f, 0x6c, 0x65, - 0x6e, 0x67, 0x74, 0x68, 0x22, 0xe4, 0x09, 0x0a, 0x0f, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, 0x12, 0xb2, 0x01, 0x0a, 0x19, 0x76, 0x69, 0x72, - 0x74, 0x75, 0x61, 0x6c, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x69, 0x70, 0x76, - 0x34, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x77, 0xba, 0x48, - 0x74, 0xba, 0x01, 0x71, 0x0a, 0x0f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x69, 0x70, 0x76, 0x34, - 0x5f, 0x63, 0x69, 0x64, 0x72, 0x12, 0x36, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x76, - 0x61, 0x6c, 0x69, 0x64, 0x20, 0x49, 0x50, 0x76, 0x34, 0x20, 0x43, 0x49, 0x44, 0x52, 0x20, 0x6e, - 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x28, 0x65, 0x2e, 0x67, 0x2e, 0x2c, 0x20, 0x27, - 0x31, 0x30, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x30, 0x2f, 0x31, 0x36, 0x27, 0x29, 0x1a, 0x26, 0x74, - 0x68, 0x69, 0x73, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, - 0x73, 0x2e, 0x69, 0x73, 0x49, 0x70, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x28, 0x34, 0x2c, 0x20, - 0x74, 0x72, 0x75, 0x65, 0x29, 0x52, 0x16, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x4e, 0x65, - 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x70, 0x76, 0x34, 0x43, 0x69, 0x64, 0x72, 0x12, 0xb0, 0x01, - 0x0a, 0x19, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, - 0x6b, 0x5f, 0x69, 0x70, 0x76, 0x36, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x75, 0xba, 0x48, 0x72, 0xba, 0x01, 0x6f, 0x0a, 0x0f, 0x76, 0x61, 0x6c, 0x69, 0x64, - 0x5f, 0x69, 0x70, 0x76, 0x36, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x12, 0x34, 0x6d, 0x75, 0x73, 0x74, - 0x20, 0x62, 0x65, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, 0x49, 0x50, 0x76, 0x36, 0x20, 0x43, - 0x49, 0x44, 0x52, 0x20, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x28, 0x65, 0x2e, - 0x67, 0x2e, 0x2c, 0x20, 0x27, 0x66, 0x64, 0x30, 0x30, 0x3a, 0x3a, 0x2f, 0x34, 0x38, 0x27, 0x29, - 0x1a, 0x26, 0x74, 0x68, 0x69, 0x73, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x20, 0x7c, 0x7c, 0x20, - 0x74, 0x68, 0x69, 0x73, 0x2e, 0x69, 0x73, 0x49, 0x70, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x28, - 0x36, 0x2c, 0x20, 0x74, 0x72, 0x75, 0x65, 0x29, 0x52, 0x16, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, - 0x6c, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x70, 0x76, 0x36, 0x43, 0x69, 0x64, 0x72, - 0x12, 0xa1, 0x01, 0x0a, 0x10, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, 0x69, 0x70, 0x76, 0x34, - 0x5f, 0x63, 0x69, 0x64, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x77, 0xba, 0x48, 0x74, - 0xba, 0x01, 0x71, 0x0a, 0x0f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x69, 0x70, 0x76, 0x34, 0x5f, - 0x63, 0x69, 0x64, 0x72, 0x12, 0x36, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x76, 0x61, - 0x6c, 0x69, 0x64, 0x20, 0x49, 0x50, 0x76, 0x34, 0x20, 0x43, 0x49, 0x44, 0x52, 0x20, 0x6e, 0x6f, - 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x28, 0x65, 0x2e, 0x67, 0x2e, 0x2c, 0x20, 0x27, 0x31, - 0x30, 0x2e, 0x30, 0x2e, 0x31, 0x2e, 0x30, 0x2f, 0x32, 0x34, 0x27, 0x29, 0x1a, 0x26, 0x74, 0x68, - 0x69, 0x73, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, - 0x2e, 0x69, 0x73, 0x49, 0x70, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x28, 0x34, 0x2c, 0x20, 0x74, - 0x72, 0x75, 0x65, 0x29, 0x52, 0x0e, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x49, 0x70, 0x76, 0x34, - 0x43, 0x69, 0x64, 0x72, 0x12, 0xa5, 0x01, 0x0a, 0x10, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, - 0x69, 0x70, 0x76, 0x36, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, - 0x7b, 0xba, 0x48, 0x78, 0xba, 0x01, 0x75, 0x0a, 0x0f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x69, - 0x70, 0x76, 0x36, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x12, 0x3a, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, - 0x65, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, 0x49, 0x50, 0x76, 0x36, 0x20, 0x43, 0x49, 0x44, - 0x52, 0x20, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x28, 0x65, 0x2e, 0x67, 0x2e, - 0x2c, 0x20, 0x27, 0x66, 0x64, 0x30, 0x30, 0x3a, 0x30, 0x3a, 0x30, 0x3a, 0x31, 0x3a, 0x3a, 0x2f, - 0x36, 0x34, 0x27, 0x29, 0x1a, 0x26, 0x74, 0x68, 0x69, 0x73, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, + 0x0a, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x0d, 0x66, 0x61, 0x62, 0x72, 0x69, 0x63, 0x4d, + 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x24, 0x0a, 0x0b, 0x6b, 0x38, 0x73, + 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, + 0x52, 0x0a, 0x6b, 0x38, 0x73, 0x4d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, + 0x35, 0x0a, 0x04, 0x73, 0x70, 0x65, 0x63, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, + 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x53, 0x70, 0x65, 0x63, + 0x52, 0x04, 0x73, 0x70, 0x65, 0x63, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x69, 0x73, 0x5f, 0x64, 0x65, + 0x66, 0x61, 0x75, 0x6c, 0x74, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x66, 0x61, 0x62, 0x72, 0x69, 0x63, + 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x6b, 0x38, 0x73, + 0x5f, 0x6d, 0x61, 0x6e, 0x61, 0x67, 0x65, 0x72, 0x22, 0x81, 0x02, 0x0a, 0x10, 0x4e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x53, 0x70, 0x65, 0x63, 0x12, 0x3c, 0x0a, + 0x08, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x20, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, + 0x73, 0x52, 0x08, 0x64, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, 0x12, 0x5c, 0x0a, 0x14, 0x64, + 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, + 0x69, 0x65, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6f, 0x73, 0x61, 0x63, + 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, + 0x74, 0x69, 0x65, 0x73, 0x52, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x43, 0x61, 0x70, + 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x11, 0x76, 0x69, 0x70, + 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x05, 0x42, 0x0a, 0xba, 0x48, 0x07, 0x1a, 0x05, 0x18, 0x80, 0x01, 0x28, 0x01, + 0x48, 0x00, 0x52, 0x0f, 0x76, 0x69, 0x70, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x4c, 0x65, 0x6e, + 0x67, 0x74, 0x68, 0x88, 0x01, 0x01, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x76, 0x69, 0x70, 0x5f, 0x70, + 0x72, 0x65, 0x66, 0x69, 0x78, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x22, 0xe4, 0x09, 0x0a, + 0x0f, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x44, 0x65, 0x66, 0x61, 0x75, 0x6c, 0x74, 0x73, + 0x12, 0xb2, 0x01, 0x0a, 0x19, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x5f, 0x6e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x69, 0x70, 0x76, 0x34, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x77, 0xba, 0x48, 0x74, 0xba, 0x01, 0x71, 0x0a, 0x0f, 0x76, 0x61, + 0x6c, 0x69, 0x64, 0x5f, 0x69, 0x70, 0x76, 0x34, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x12, 0x36, 0x6d, + 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, 0x49, 0x50, 0x76, + 0x34, 0x20, 0x43, 0x49, 0x44, 0x52, 0x20, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, + 0x28, 0x65, 0x2e, 0x67, 0x2e, 0x2c, 0x20, 0x27, 0x31, 0x30, 0x2e, 0x30, 0x2e, 0x30, 0x2e, 0x30, + 0x2f, 0x31, 0x36, 0x27, 0x29, 0x1a, 0x26, 0x74, 0x68, 0x69, 0x73, 0x20, 0x3d, 0x3d, 0x20, 0x27, + 0x27, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x69, 0x73, 0x49, 0x70, 0x50, 0x72, + 0x65, 0x66, 0x69, 0x78, 0x28, 0x34, 0x2c, 0x20, 0x74, 0x72, 0x75, 0x65, 0x29, 0x52, 0x16, 0x76, + 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x49, 0x70, 0x76, + 0x34, 0x43, 0x69, 0x64, 0x72, 0x12, 0xb0, 0x01, 0x0a, 0x19, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, + 0x6c, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x69, 0x70, 0x76, 0x36, 0x5f, 0x63, + 0x69, 0x64, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x75, 0xba, 0x48, 0x72, 0xba, 0x01, + 0x6f, 0x0a, 0x0f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x69, 0x70, 0x76, 0x36, 0x5f, 0x63, 0x69, + 0x64, 0x72, 0x12, 0x34, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x20, 0x49, 0x50, 0x76, 0x36, 0x20, 0x43, 0x49, 0x44, 0x52, 0x20, 0x6e, 0x6f, 0x74, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x28, 0x65, 0x2e, 0x67, 0x2e, 0x2c, 0x20, 0x27, 0x66, 0x64, 0x30, + 0x30, 0x3a, 0x3a, 0x2f, 0x34, 0x38, 0x27, 0x29, 0x1a, 0x26, 0x74, 0x68, 0x69, 0x73, 0x20, 0x3d, + 0x3d, 0x20, 0x27, 0x27, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x69, 0x73, 0x49, + 0x70, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x28, 0x36, 0x2c, 0x20, 0x74, 0x72, 0x75, 0x65, 0x29, + 0x52, 0x16, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x49, 0x70, 0x76, 0x36, 0x43, 0x69, 0x64, 0x72, 0x12, 0xa1, 0x01, 0x0a, 0x10, 0x73, 0x75, 0x62, + 0x6e, 0x65, 0x74, 0x5f, 0x69, 0x70, 0x76, 0x34, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x77, 0xba, 0x48, 0x74, 0xba, 0x01, 0x71, 0x0a, 0x0f, 0x76, 0x61, 0x6c, + 0x69, 0x64, 0x5f, 0x69, 0x70, 0x76, 0x34, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x12, 0x36, 0x6d, 0x75, + 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, 0x49, 0x50, 0x76, 0x34, + 0x20, 0x43, 0x49, 0x44, 0x52, 0x20, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x28, + 0x65, 0x2e, 0x67, 0x2e, 0x2c, 0x20, 0x27, 0x31, 0x30, 0x2e, 0x30, 0x2e, 0x31, 0x2e, 0x30, 0x2f, + 0x32, 0x34, 0x27, 0x29, 0x1a, 0x26, 0x74, 0x68, 0x69, 0x73, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x69, 0x73, 0x49, 0x70, 0x50, 0x72, 0x65, - 0x66, 0x69, 0x78, 0x28, 0x36, 0x2c, 0x20, 0x74, 0x72, 0x75, 0x65, 0x29, 0x52, 0x0e, 0x73, 0x75, - 0x62, 0x6e, 0x65, 0x74, 0x49, 0x70, 0x76, 0x36, 0x43, 0x69, 0x64, 0x72, 0x12, 0x42, 0x0a, 0x0d, - 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x03, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x52, 0x75, - 0x6c, 0x65, 0x52, 0x0c, 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x52, 0x75, 0x6c, 0x65, 0x73, - 0x12, 0x40, 0x0a, 0x0c, 0x65, 0x67, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x73, - 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, - 0x79, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0b, 0x65, 0x67, 0x72, 0x65, 0x73, 0x73, 0x52, 0x75, 0x6c, - 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x74, - 0x5f, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, - 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x4e, 0x61, 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, - 0x3a, 0xe8, 0x02, 0xba, 0x48, 0xe4, 0x02, 0x1a, 0xaf, 0x01, 0x0a, 0x29, 0x73, 0x75, 0x62, 0x6e, - 0x65, 0x74, 0x5f, 0x69, 0x70, 0x76, 0x34, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, - 0x5f, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x5f, 0x69, 0x70, 0x76, 0x34, 0x12, 0x3d, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, 0x69, 0x70, - 0x76, 0x34, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, - 0x20, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, - 0x5f, 0x69, 0x70, 0x76, 0x34, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, - 0x20, 0x73, 0x65, 0x74, 0x1a, 0x43, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x73, 0x75, 0x62, 0x6e, 0x65, - 0x74, 0x5f, 0x69, 0x70, 0x76, 0x34, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x20, 0x3d, 0x3d, 0x20, 0x27, - 0x27, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, - 0x6c, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x69, 0x70, 0x76, 0x34, 0x5f, 0x63, - 0x69, 0x64, 0x72, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x1a, 0xaf, 0x01, 0x0a, 0x29, 0x73, 0x75, - 0x62, 0x6e, 0x65, 0x74, 0x5f, 0x69, 0x70, 0x76, 0x36, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, - 0x65, 0x73, 0x5f, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x5f, 0x69, 0x70, 0x76, 0x36, 0x12, 0x3d, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, - 0x69, 0x70, 0x76, 0x36, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, - 0x65, 0x73, 0x20, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x5f, 0x69, 0x70, 0x76, 0x36, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x20, 0x74, 0x6f, 0x20, - 0x62, 0x65, 0x20, 0x73, 0x65, 0x74, 0x1a, 0x43, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x73, 0x75, 0x62, - 0x6e, 0x65, 0x74, 0x5f, 0x69, 0x70, 0x76, 0x36, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x20, 0x3d, 0x3d, - 0x20, 0x27, 0x27, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x76, 0x69, 0x72, 0x74, - 0x75, 0x61, 0x6c, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x69, 0x70, 0x76, 0x36, - 0x5f, 0x63, 0x69, 0x64, 0x72, 0x20, 0x21, 0x3d, 0x20, 0x27, 0x27, 0x22, 0x19, 0x0a, 0x17, 0x4e, - 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x43, 0x6f, 0x6e, 0x73, 0x74, - 0x72, 0x61, 0x69, 0x6e, 0x74, 0x73, 0x22, 0xb5, 0x01, 0x0a, 0x18, 0x4e, 0x65, 0x74, 0x77, 0x6f, - 0x72, 0x6b, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, - 0x69, 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x5f, - 0x69, 0x70, 0x76, 0x34, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x73, 0x75, 0x70, 0x70, - 0x6f, 0x72, 0x74, 0x73, 0x49, 0x70, 0x76, 0x34, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x75, 0x70, 0x70, - 0x6f, 0x72, 0x74, 0x73, 0x5f, 0x69, 0x70, 0x76, 0x36, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, - 0x0c, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x49, 0x70, 0x76, 0x36, 0x12, 0x2e, 0x0a, - 0x13, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x5f, 0x64, 0x75, 0x61, 0x6c, 0x5f, 0x73, - 0x74, 0x61, 0x63, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x73, 0x75, 0x70, 0x70, - 0x6f, 0x72, 0x74, 0x73, 0x44, 0x75, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x63, 0x6b, 0x12, 0x1f, 0x0a, - 0x0b, 0x64, 0x70, 0x75, 0x5f, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, - 0x28, 0x08, 0x52, 0x0a, 0x64, 0x70, 0x75, 0x53, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x8b, - 0x01, 0x0a, 0x12, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x38, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, - 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6c, - 0x61, 0x73, 0x73, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x1d, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x48, 0x00, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x88, 0x01, 0x01, 0x12, 0x10, - 0x0a, 0x03, 0x68, 0x75, 0x62, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x68, 0x75, 0x62, - 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2a, 0x98, 0x01, 0x0a, - 0x11, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x53, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x23, 0x0a, 0x1f, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x43, 0x4c, - 0x41, 0x53, 0x53, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, - 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1f, 0x0a, 0x1b, 0x4e, 0x45, 0x54, 0x57, 0x4f, - 0x52, 0x4b, 0x5f, 0x43, 0x4c, 0x41, 0x53, 0x53, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x50, - 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x1d, 0x0a, 0x19, 0x4e, 0x45, 0x54, 0x57, - 0x4f, 0x52, 0x4b, 0x5f, 0x43, 0x4c, 0x41, 0x53, 0x53, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, - 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, 0x02, 0x12, 0x1e, 0x0a, 0x1a, 0x4e, 0x45, 0x54, 0x57, 0x4f, - 0x52, 0x4b, 0x5f, 0x43, 0x4c, 0x41, 0x53, 0x53, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x46, - 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x42, 0xda, 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, - 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x42, - 0x15, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x54, 0x79, 0x70, - 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, - 0x74, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2f, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6f, 0x73, 0x61, - 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x70, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4f, 0x50, 0x58, 0xaa, 0x02, 0x0f, 0x4f, - 0x73, 0x61, 0x63, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, - 0x10, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, - 0x31, 0xe2, 0x02, 0x1c, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x5f, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, - 0xea, 0x02, 0x11, 0x4f, 0x73, 0x61, 0x63, 0x3a, 0x3a, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x66, 0x69, 0x78, 0x28, 0x34, 0x2c, 0x20, 0x74, 0x72, 0x75, 0x65, 0x29, 0x52, 0x0e, 0x73, 0x75, + 0x62, 0x6e, 0x65, 0x74, 0x49, 0x70, 0x76, 0x34, 0x43, 0x69, 0x64, 0x72, 0x12, 0xa5, 0x01, 0x0a, + 0x10, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, 0x69, 0x70, 0x76, 0x36, 0x5f, 0x63, 0x69, 0x64, + 0x72, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x42, 0x7b, 0xba, 0x48, 0x78, 0xba, 0x01, 0x75, 0x0a, + 0x0f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x5f, 0x69, 0x70, 0x76, 0x36, 0x5f, 0x63, 0x69, 0x64, 0x72, + 0x12, 0x3a, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x20, + 0x49, 0x50, 0x76, 0x36, 0x20, 0x43, 0x49, 0x44, 0x52, 0x20, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x20, 0x28, 0x65, 0x2e, 0x67, 0x2e, 0x2c, 0x20, 0x27, 0x66, 0x64, 0x30, 0x30, 0x3a, + 0x30, 0x3a, 0x30, 0x3a, 0x31, 0x3a, 0x3a, 0x2f, 0x36, 0x34, 0x27, 0x29, 0x1a, 0x26, 0x74, 0x68, + 0x69, 0x73, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, 0x73, + 0x2e, 0x69, 0x73, 0x49, 0x70, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x28, 0x36, 0x2c, 0x20, 0x74, + 0x72, 0x75, 0x65, 0x29, 0x52, 0x0e, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x49, 0x70, 0x76, 0x36, + 0x43, 0x69, 0x64, 0x72, 0x12, 0x42, 0x0a, 0x0d, 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x5f, + 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x6f, 0x73, + 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x65, + 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0c, 0x69, 0x6e, 0x67, 0x72, + 0x65, 0x73, 0x73, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x40, 0x0a, 0x0c, 0x65, 0x67, 0x72, 0x65, + 0x73, 0x73, 0x5f, 0x72, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, + 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, + 0x2e, 0x53, 0x65, 0x63, 0x75, 0x72, 0x69, 0x74, 0x79, 0x52, 0x75, 0x6c, 0x65, 0x52, 0x0b, 0x65, + 0x67, 0x72, 0x65, 0x73, 0x73, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x65, 0x6e, + 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x6e, 0x61, 0x74, 0x5f, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x65, 0x6e, 0x61, 0x62, 0x6c, 0x65, 0x4e, 0x61, + 0x74, 0x47, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x3a, 0xe8, 0x02, 0xba, 0x48, 0xe4, 0x02, 0x1a, + 0xaf, 0x01, 0x0a, 0x29, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, 0x69, 0x70, 0x76, 0x34, 0x5f, + 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, + 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x69, 0x70, 0x76, 0x34, 0x12, 0x3d, 0x73, + 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, 0x69, 0x70, 0x76, 0x34, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x20, + 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x20, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, + 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x69, 0x70, 0x76, 0x34, 0x5f, 0x63, 0x69, + 0x64, 0x72, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x74, 0x1a, 0x43, 0x74, 0x68, + 0x69, 0x73, 0x2e, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, 0x69, 0x70, 0x76, 0x34, 0x5f, 0x63, + 0x69, 0x64, 0x72, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x20, 0x7c, 0x7c, 0x20, 0x74, 0x68, 0x69, + 0x73, 0x2e, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x5f, 0x69, 0x70, 0x76, 0x34, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x20, 0x21, 0x3d, 0x20, 0x27, + 0x27, 0x1a, 0xaf, 0x01, 0x0a, 0x29, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, 0x69, 0x70, 0x76, + 0x36, 0x5f, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x5f, 0x76, 0x69, 0x72, 0x74, 0x75, + 0x61, 0x6c, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x69, 0x70, 0x76, 0x36, 0x12, + 0x3d, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, 0x69, 0x70, 0x76, 0x36, 0x5f, 0x63, 0x69, 0x64, + 0x72, 0x20, 0x72, 0x65, 0x71, 0x75, 0x69, 0x72, 0x65, 0x73, 0x20, 0x76, 0x69, 0x72, 0x74, 0x75, + 0x61, 0x6c, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x5f, 0x69, 0x70, 0x76, 0x36, 0x5f, + 0x63, 0x69, 0x64, 0x72, 0x20, 0x74, 0x6f, 0x20, 0x62, 0x65, 0x20, 0x73, 0x65, 0x74, 0x1a, 0x43, + 0x74, 0x68, 0x69, 0x73, 0x2e, 0x73, 0x75, 0x62, 0x6e, 0x65, 0x74, 0x5f, 0x69, 0x70, 0x76, 0x36, + 0x5f, 0x63, 0x69, 0x64, 0x72, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x20, 0x7c, 0x7c, 0x20, 0x74, + 0x68, 0x69, 0x73, 0x2e, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c, 0x5f, 0x6e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x5f, 0x69, 0x70, 0x76, 0x36, 0x5f, 0x63, 0x69, 0x64, 0x72, 0x20, 0x21, 0x3d, + 0x20, 0x27, 0x27, 0x22, 0x19, 0x0a, 0x17, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6c, + 0x61, 0x73, 0x73, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, 0x73, 0x22, 0xb5, + 0x01, 0x0a, 0x18, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x43, + 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x73, + 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x5f, 0x69, 0x70, 0x76, 0x34, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x0c, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x49, 0x70, 0x76, 0x34, + 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x5f, 0x69, 0x70, 0x76, + 0x36, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, + 0x73, 0x49, 0x70, 0x76, 0x36, 0x12, 0x2e, 0x0a, 0x13, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, + 0x73, 0x5f, 0x64, 0x75, 0x61, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x63, 0x6b, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x11, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x44, 0x75, 0x61, 0x6c, + 0x53, 0x74, 0x61, 0x63, 0x6b, 0x12, 0x1f, 0x0a, 0x0b, 0x64, 0x70, 0x75, 0x5f, 0x73, 0x75, 0x70, + 0x70, 0x6f, 0x72, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x64, 0x70, 0x75, 0x53, + 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x22, 0x8b, 0x01, 0x0a, 0x12, 0x4e, 0x65, 0x74, 0x77, 0x6f, + 0x72, 0x6b, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x38, 0x0a, + 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x22, 0x2e, 0x6f, + 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x4e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x43, 0x6c, 0x61, 0x73, 0x73, 0x53, 0x74, 0x61, 0x74, 0x65, + 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1d, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, + 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x88, 0x01, 0x01, 0x12, 0x10, 0x0a, 0x03, 0x68, 0x75, 0x62, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x68, 0x75, 0x62, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x2a, 0x98, 0x01, 0x0a, 0x11, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x43, 0x6c, 0x61, 0x73, 0x73, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x23, 0x0a, 0x1f, 0x4e, 0x45, + 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x43, 0x4c, 0x41, 0x53, 0x53, 0x5f, 0x53, 0x54, 0x41, 0x54, + 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, + 0x1f, 0x0a, 0x1b, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x43, 0x4c, 0x41, 0x53, 0x53, + 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, + 0x12, 0x1d, 0x0a, 0x19, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x43, 0x4c, 0x41, 0x53, + 0x53, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x59, 0x10, 0x02, 0x12, + 0x1e, 0x0a, 0x1a, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x43, 0x4c, 0x41, 0x53, 0x53, + 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x42, + 0xda, 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x15, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x43, 0x6c, 0x61, 0x73, 0x73, 0x54, 0x79, 0x70, 0x65, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, + 0x5a, 0x4c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x73, 0x61, + 0x63, 0x2d, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x6d, + 0x65, 0x74, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, + 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x76, 0x31, 0xa2, 0x02, + 0x03, 0x4f, 0x50, 0x58, 0xaa, 0x02, 0x0f, 0x4f, 0x73, 0x61, 0x63, 0x2e, 0x50, 0x72, 0x69, 0x76, + 0x61, 0x74, 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x10, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1c, 0x4f, 0x73, 0x61, 0x63, + 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, + 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x4f, 0x73, 0x61, 0x63, 0x3a, + 0x3a, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x33, }) var ( diff --git a/osac-metering/metering-service/internal/api/osac/private/v1/project_type.pb.go b/osac-metering/metering-service/internal/api/osac/private/v1/project_type.pb.go index d8530bcc2..c706cd883 100644 --- a/osac-metering/metering-service/internal/api/osac/private/v1/project_type.pb.go +++ b/osac-metering/metering-service/internal/api/osac/private/v1/project_type.pb.go @@ -229,7 +229,7 @@ type Project struct { // // Project names follow a hierarchical dot-separated DNS label format (e.g., "org.team.project"). // Each segment must be a valid DNS label: max 63 characters, lowercase letters (a-z), digits (0-9), - // and hyphens, with alphanumeric start and end. Empty name is allowed for the root project. + // and hyphens, with alphanumeric start and end. // // Note: Uses (buf.validate.field).ignore = IGNORE_ALWAYS to skip standard Metadata.name pattern // validation (which doesn't allow dots). Project-specific validation is handled by message-level CEL. @@ -547,7 +547,7 @@ var file_osac_private_v1_project_type_proto_rawDesc = string([]byte{ 0x61, 0x74, 0x75, 0x73, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x23, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa9, 0x04, 0x0a, 0x07, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, + 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x96, 0x04, 0x0a, 0x07, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x3d, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, @@ -559,95 +559,94 @@ var file_osac_private_v1_project_type_proto_rawDesc = string([]byte{ 0x63, 0x12, 0x36, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, - 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x3a, 0xe4, 0x02, 0xba, 0x48, 0xe0, 0x02, - 0x1a, 0xdd, 0x02, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, - 0x65, 0x5f, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x94, 0x01, 0x70, 0x72, 0x6f, - 0x6a, 0x65, 0x63, 0x74, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, - 0x65, 0x20, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x20, 0x28, 0x72, 0x6f, 0x6f, 0x74, 0x20, 0x70, 0x72, - 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x29, 0x20, 0x6f, 0x72, 0x20, 0x64, 0x6f, 0x74, 0x2d, 0x73, 0x65, - 0x70, 0x61, 0x72, 0x61, 0x74, 0x65, 0x64, 0x20, 0x44, 0x4e, 0x53, 0x20, 0x6c, 0x61, 0x62, 0x65, - 0x6c, 0x73, 0x20, 0x28, 0x65, 0x61, 0x63, 0x68, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, - 0x3a, 0x20, 0x6d, 0x61, 0x78, 0x20, 0x36, 0x33, 0x20, 0x63, 0x68, 0x61, 0x72, 0x73, 0x2c, 0x20, - 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x63, 0x61, 0x73, 0x65, 0x20, 0x61, 0x2d, 0x7a, 0x2f, 0x30, 0x2d, - 0x39, 0x2f, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x2c, 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, - 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0x2f, 0x65, 0x6e, 0x64, - 0x29, 0x1a, 0xac, 0x01, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x27, 0x20, 0x7c, 0x7c, 0x20, - 0x74, 0x68, 0x69, 0x73, 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x6e, 0x61, - 0x6d, 0x65, 0x2e, 0x73, 0x70, 0x6c, 0x69, 0x74, 0x28, 0x27, 0x2e, 0x27, 0x29, 0x2e, 0x61, 0x6c, - 0x6c, 0x28, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x2c, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x73, 0x69, 0x7a, 0x65, 0x28, 0x29, 0x20, 0x3e, 0x20, 0x30, 0x20, 0x26, 0x26, - 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x73, 0x69, 0x7a, 0x65, 0x28, 0x29, 0x20, - 0x3c, 0x3d, 0x20, 0x36, 0x33, 0x20, 0x26, 0x26, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, - 0x2e, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, - 0x2d, 0x39, 0x5d, 0x28, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x2d, 0x5d, 0x7b, 0x30, 0x2c, - 0x36, 0x31, 0x7d, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x27, 0x29, 0x29, - 0x22, 0x5a, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x70, 0x65, 0x63, 0x12, - 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x25, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, - 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x64, 0x65, - 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x42, 0x0e, 0x0a, 0x0c, - 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xb2, 0x01, 0x0a, - 0x0d, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x33, - 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, - 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, - 0x61, 0x74, 0x65, 0x12, 0x1d, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x88, - 0x01, 0x01, 0x12, 0x41, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, - 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, - 0x65, 0x22, 0xda, 0x01, 0x0a, 0x10, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, - 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0e, 0x32, 0x25, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, - 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, - 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x12, 0x38, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0e, 0x32, 0x20, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, - 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1b, 0x0a, 0x06, 0x72, - 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x72, - 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, - 0x61, 0x67, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x07, 0x6d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x72, 0x65, 0x61, 0x73, - 0x6f, 0x6e, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2a, 0xb9, - 0x01, 0x0a, 0x0c, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, - 0x1d, 0x0a, 0x19, 0x50, 0x52, 0x4f, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, - 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x19, - 0x0a, 0x15, 0x50, 0x52, 0x4f, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, - 0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x50, 0x52, 0x4f, - 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, - 0x45, 0x10, 0x02, 0x12, 0x18, 0x0a, 0x14, 0x50, 0x52, 0x4f, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x53, - 0x54, 0x41, 0x54, 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x12, 0x1a, 0x0a, - 0x16, 0x50, 0x52, 0x4f, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x44, - 0x45, 0x4c, 0x45, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x04, 0x12, 0x1f, 0x0a, 0x1b, 0x50, 0x52, 0x4f, - 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, - 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x2a, 0x8d, 0x01, 0x0a, 0x14, 0x50, - 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, - 0x79, 0x70, 0x65, 0x12, 0x26, 0x0a, 0x22, 0x50, 0x52, 0x4f, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x43, - 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, - 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x23, 0x0a, 0x1f, 0x50, - 0x52, 0x4f, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, - 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x48, 0x55, 0x42, 0x5f, 0x53, 0x59, 0x4e, 0x43, 0x10, 0x01, - 0x12, 0x28, 0x0a, 0x24, 0x50, 0x52, 0x4f, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x43, 0x4f, 0x4e, 0x44, - 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4b, 0x45, 0x59, 0x43, 0x4c, - 0x4f, 0x41, 0x4b, 0x5f, 0x53, 0x59, 0x4e, 0x43, 0x10, 0x02, 0x42, 0xd5, 0x01, 0x0a, 0x13, 0x63, - 0x6f, 0x6d, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, - 0x76, 0x31, 0x42, 0x10, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, 0x50, - 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, - 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2f, 0x69, 0x6e, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2f, - 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x4f, 0x50, 0x58, 0xaa, 0x02, 0x0f, 0x4f, 0x73, 0x61, - 0x63, 0x2e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x10, 0x4f, - 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0xe2, - 0x02, 0x1c, 0x4f, 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, - 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, - 0x11, 0x4f, 0x73, 0x61, 0x63, 0x3a, 0x3a, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x3a, 0x3a, - 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x3a, 0xd1, 0x02, 0xba, 0x48, 0xcd, 0x02, + 0x1a, 0xca, 0x02, 0x0a, 0x15, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x5f, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x7c, 0x70, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, + 0x20, 0x64, 0x6f, 0x74, 0x2d, 0x73, 0x65, 0x70, 0x61, 0x72, 0x61, 0x74, 0x65, 0x64, 0x20, 0x44, + 0x4e, 0x53, 0x20, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x20, 0x28, 0x65, 0x61, 0x63, 0x68, 0x20, + 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x3a, 0x20, 0x6d, 0x61, 0x78, 0x20, 0x36, 0x33, 0x20, + 0x63, 0x68, 0x61, 0x72, 0x73, 0x2c, 0x20, 0x6c, 0x6f, 0x77, 0x65, 0x72, 0x63, 0x61, 0x73, 0x65, + 0x20, 0x61, 0x2d, 0x7a, 0x2f, 0x30, 0x2d, 0x39, 0x2f, 0x68, 0x79, 0x70, 0x68, 0x65, 0x6e, 0x2c, + 0x20, 0x61, 0x6c, 0x70, 0x68, 0x61, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x69, 0x63, 0x20, 0x73, 0x74, + 0x61, 0x72, 0x74, 0x2f, 0x65, 0x6e, 0x64, 0x29, 0x1a, 0xb2, 0x01, 0x74, 0x68, 0x69, 0x73, 0x2e, + 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x2e, 0x73, 0x69, + 0x7a, 0x65, 0x28, 0x29, 0x20, 0x3e, 0x20, 0x30, 0x20, 0x26, 0x26, 0x20, 0x74, 0x68, 0x69, 0x73, + 0x2e, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x6e, 0x61, 0x6d, 0x65, 0x2e, 0x73, + 0x70, 0x6c, 0x69, 0x74, 0x28, 0x27, 0x2e, 0x27, 0x29, 0x2e, 0x61, 0x6c, 0x6c, 0x28, 0x73, 0x65, + 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x2c, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x73, + 0x69, 0x7a, 0x65, 0x28, 0x29, 0x20, 0x3e, 0x20, 0x30, 0x20, 0x26, 0x26, 0x20, 0x73, 0x65, 0x67, + 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x73, 0x69, 0x7a, 0x65, 0x28, 0x29, 0x20, 0x3c, 0x3d, 0x20, 0x36, + 0x33, 0x20, 0x26, 0x26, 0x20, 0x73, 0x65, 0x67, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x6d, 0x61, 0x74, + 0x63, 0x68, 0x65, 0x73, 0x28, 0x27, 0x5e, 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x28, + 0x5b, 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x2d, 0x5d, 0x7b, 0x30, 0x2c, 0x36, 0x31, 0x7d, 0x5b, + 0x61, 0x2d, 0x7a, 0x30, 0x2d, 0x39, 0x5d, 0x29, 0x3f, 0x24, 0x27, 0x29, 0x29, 0x22, 0x5a, 0x0a, + 0x0b, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x70, 0x65, 0x63, 0x12, 0x14, 0x0a, 0x05, + 0x74, 0x69, 0x74, 0x6c, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, + 0x6c, 0x65, 0x12, 0x25, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x64, 0x65, + 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0xb2, 0x01, 0x0a, 0x0d, 0x50, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x33, 0x0a, 0x05, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1d, 0x2e, 0x6f, 0x73, 0x61, + 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x12, 0x1d, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x48, 0x00, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x88, 0x01, 0x01, 0x12, + 0x41, 0x0a, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, + 0x74, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, + 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, 0xda, + 0x01, 0x0a, 0x10, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x39, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x25, 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x38, + 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, + 0x2e, 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, + 0x2e, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1b, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x42, + 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2a, 0xb9, 0x01, 0x0a, 0x0c, + 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1d, 0x0a, 0x19, + 0x50, 0x52, 0x4f, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, + 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x19, 0x0a, 0x15, 0x50, + 0x52, 0x4f, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x50, 0x45, 0x4e, + 0x44, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x18, 0x0a, 0x14, 0x50, 0x52, 0x4f, 0x4a, 0x45, 0x43, + 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x41, 0x43, 0x54, 0x49, 0x56, 0x45, 0x10, 0x02, + 0x12, 0x18, 0x0a, 0x14, 0x50, 0x52, 0x4f, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, + 0x45, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x03, 0x12, 0x1a, 0x0a, 0x16, 0x50, 0x52, + 0x4f, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x45, + 0x54, 0x49, 0x4e, 0x47, 0x10, 0x04, 0x12, 0x1f, 0x0a, 0x1b, 0x50, 0x52, 0x4f, 0x4a, 0x45, 0x43, + 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x44, 0x45, 0x4c, 0x45, 0x54, 0x45, 0x5f, 0x46, + 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x05, 0x2a, 0x8d, 0x01, 0x0a, 0x14, 0x50, 0x72, 0x6f, 0x6a, + 0x65, 0x63, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x26, 0x0a, 0x22, 0x50, 0x52, 0x4f, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x43, 0x4f, 0x4e, 0x44, + 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, + 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x23, 0x0a, 0x1f, 0x50, 0x52, 0x4f, 0x4a, + 0x45, 0x43, 0x54, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x48, 0x55, 0x42, 0x5f, 0x53, 0x59, 0x4e, 0x43, 0x10, 0x01, 0x12, 0x28, 0x0a, + 0x24, 0x50, 0x52, 0x4f, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x43, 0x4f, 0x4e, 0x44, 0x49, 0x54, 0x49, + 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4b, 0x45, 0x59, 0x43, 0x4c, 0x4f, 0x41, 0x4b, + 0x5f, 0x53, 0x59, 0x4e, 0x43, 0x10, 0x02, 0x42, 0xd5, 0x01, 0x0a, 0x13, 0x63, 0x6f, 0x6d, 0x2e, + 0x6f, 0x73, 0x61, 0x63, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x76, 0x31, 0x42, + 0x10, 0x50, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, 0x50, 0x72, 0x6f, 0x74, + 0x6f, 0x50, 0x01, 0x5a, 0x4c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, + 0x6f, 0x73, 0x61, 0x63, 0x2d, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x2f, 0x6f, 0x73, 0x61, + 0x63, 0x2d, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x69, 0x6e, 0x67, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x6f, 0x73, 0x61, 0x63, 0x2f, 0x70, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x76, + 0x31, 0xa2, 0x02, 0x03, 0x4f, 0x50, 0x58, 0xaa, 0x02, 0x0f, 0x4f, 0x73, 0x61, 0x63, 0x2e, 0x50, + 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x10, 0x4f, 0x73, 0x61, 0x63, + 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x1c, 0x4f, + 0x73, 0x61, 0x63, 0x5c, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x5c, 0x56, 0x31, 0x5c, + 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x11, 0x4f, 0x73, + 0x61, 0x63, 0x3a, 0x3a, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, }) var ( diff --git a/osac-metering/metering-service/internal/database/migrations/0_create_metering_resource_state.up.sql b/osac-metering/metering-service/internal/database/migrations/0_create_metering_resource_state.up.sql index e7302c7cf..26f28dbdd 100644 --- a/osac-metering/metering-service/internal/database/migrations/0_create_metering_resource_state.up.sql +++ b/osac-metering/metering-service/internal/database/migrations/0_create_metering_resource_state.up.sql @@ -18,6 +18,7 @@ CREATE TABLE metering_resource_state ( transition_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), fulfillment_version INT NOT NULL, billing_dimensions JSONB NOT NULL DEFAULT '{}'::JSONB, + component_billable_since JSONB NOT NULL DEFAULT '{}'::JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); diff --git a/osac-metering/metering-service/internal/events/cluster.go b/osac-metering/metering-service/internal/events/cluster.go new file mode 100644 index 000000000..a8b1f5338 --- /dev/null +++ b/osac-metering/metering-service/internal/events/cluster.go @@ -0,0 +1,419 @@ +/* +Copyright (c) 2026 Red Hat, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +in compliance with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 +*/ + +package events + +import ( + "fmt" + "sort" + "strings" + "time" + + cloudevents "github.com/cloudevents/sdk-go/v2" + + privatev1 "github.com/osac-project/osac-metering/internal/api/osac/private/v1" +) + +const ClusterStatePrefix = "CLUSTER_STATE_" + +// DimensionReleaseImage is the billing dimension key for the cluster release image. +const DimensionReleaseImage = "release_image" + +// CaaS cluster state constants. +const ( + ClusterStateProgressing = "PROGRESSING" + ClusterStateReady = "READY" + ClusterStateFailed = "FAILED" + ClusterStateDeleting = "DELETING" + ClusterStateDeleteFailed = "DELETE_FAILED" + ClusterStateUnspecified = "UNSPECIFIED" +) + +type clusterMapper struct { + cl *privatev1.Cluster +} + +func (m *clusterMapper) ResourceType() string { return ResourceTypeClusterOrder } +func (m *clusterMapper) ResourceID() string { return m.cl.GetId() } + +func (m *clusterMapper) FulfillmentVersion() int32 { + if md := m.cl.GetMetadata(); md != nil { + return md.GetVersion() + } + return 0 +} + +func (m *clusterMapper) TenantID() string { + if md := m.cl.GetMetadata(); md != nil { + return md.GetTenant() + } + return "" +} + +func (m *clusterMapper) ProjectID() *string { + if md := m.cl.GetMetadata(); md != nil { + return NilIfEmpty(md.GetProject()) + } + return nil +} + +func (m *clusterMapper) CatalogItemID() *string { + if s := m.cl.GetSpec(); s != nil { + if ci := s.GetCatalogItem(); ci != nil { + return NilIfEmpty(ci.GetId()) + } + } + return nil +} + +func (m *clusterMapper) TemplateID() *string { + if s := m.cl.GetSpec(); s != nil { + if t := s.GetTemplate(); t != nil { + return NilIfEmpty(t.GetId()) + } + } + return nil +} + +func (m *clusterMapper) CurrentState() string { + state := privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED + if s := m.cl.GetStatus(); s != nil { + state = s.GetState() + } + return strings.TrimPrefix(state.String(), ClusterStatePrefix) +} + +func (m *clusterMapper) IsBillable() bool { + return IsClusterBillableState(m.CurrentState()) +} + +func (m *clusterMapper) BillingDimensionsMap() map[string]any { + return ClusterBillingDimensions(m.cl) +} + +// CaaS cluster state machine. Both PROGRESSING and READY are billable. +// Transitions between billable states have no billing boundary (Skip). +// Dimension changes (scaling) during skipped transitions are detected by +// the Watch Consumer via DimensionsEqual; the hourly reconciler catches +// any missed dimension drift. +var clusterTransitions = TransitionTable{ + // Started: first billable state (no previous) + {StateEmpty, ClusterStateProgressing}: {EventType: EventStarted}, + {StateEmpty, ClusterStateReady}: {EventType: EventStarted}, + + // Skip: first observed in non-billable state (bootstrap, reconnect after failure) + {StateEmpty, ClusterStateFailed}: {Skip: true}, + {StateEmpty, ClusterStateDeleting}: {Skip: true}, + {StateEmpty, ClusterStateDeleteFailed}: {Skip: true}, + {StateEmpty, ClusterStateUnspecified}: {Skip: true}, + + // Resumed: non-billable to billable + {ClusterStateFailed, ClusterStateProgressing}: {EventType: EventResumed}, + {ClusterStateFailed, ClusterStateReady}: {EventType: EventResumed}, + {ClusterStateDeleting, ClusterStateProgressing}: {EventType: EventResumed}, + {ClusterStateDeleting, ClusterStateReady}: {EventType: EventResumed}, + {ClusterStateDeleteFailed, ClusterStateProgressing}: {EventType: EventResumed}, + {ClusterStateDeleteFailed, ClusterStateReady}: {EventType: EventResumed}, + {ClusterStateUnspecified, ClusterStateProgressing}: {EventType: EventResumed}, + {ClusterStateUnspecified, ClusterStateReady}: {EventType: EventResumed}, + + // Suspended: billable to non-billable + {ClusterStateProgressing, ClusterStateFailed}: {EventType: EventSuspended}, + {ClusterStateProgressing, ClusterStateDeleting}: {EventType: EventSuspended}, + {ClusterStateReady, ClusterStateFailed}: {EventType: EventSuspended}, + {ClusterStateReady, ClusterStateDeleting}: {EventType: EventSuspended}, + {ClusterStateProgressing, ClusterStateUnspecified}: {EventType: EventSuspended}, + {ClusterStateReady, ClusterStateUnspecified}: {EventType: EventSuspended}, + {ClusterStateReady, ClusterStateDeleteFailed}: {EventType: EventSuspended}, + {ClusterStateProgressing, ClusterStateDeleteFailed}: {EventType: EventSuspended}, + + // Skip: billable to billable (no billing boundary, includes same-state for scaling) + {ClusterStateProgressing, ClusterStateReady}: {Skip: true}, + {ClusterStateReady, ClusterStateProgressing}: {Skip: true}, + {ClusterStateProgressing, ClusterStateProgressing}: {Skip: true}, + {ClusterStateReady, ClusterStateReady}: {Skip: true}, + + // Skip: non-billable to non-billable (no billing effect, includes same-state) + {ClusterStateFailed, ClusterStateFailed}: {Skip: true}, + {ClusterStateDeleting, ClusterStateDeleting}: {Skip: true}, + {ClusterStateDeleteFailed, ClusterStateDeleteFailed}: {Skip: true}, + {ClusterStateUnspecified, ClusterStateUnspecified}: {Skip: true}, + {ClusterStateFailed, ClusterStateDeleting}: {Skip: true}, + {ClusterStateFailed, ClusterStateDeleteFailed}: {Skip: true}, + {ClusterStateDeleting, ClusterStateDeleteFailed}: {Skip: true}, + {ClusterStateDeleteFailed, ClusterStateDeleting}: {Skip: true}, + {ClusterStateDeleting, ClusterStateFailed}: {Skip: true}, + {ClusterStateDeleteFailed, ClusterStateFailed}: {Skip: true}, + {ClusterStateUnspecified, ClusterStateFailed}: {Skip: true}, + {ClusterStateUnspecified, ClusterStateDeleting}: {Skip: true}, + {ClusterStateUnspecified, ClusterStateDeleteFailed}: {Skip: true}, + {ClusterStateFailed, ClusterStateUnspecified}: {Skip: true}, + {ClusterStateDeleting, ClusterStateUnspecified}: {Skip: true}, + {ClusterStateDeleteFailed, ClusterStateUnspecified}: {Skip: true}, +} + +func (m *clusterMapper) CloudEventType(eventType privatev1.EventType, previousState string) (string, error) { + return ResolveCloudEventType(clusterTransitions, eventType, previousState, m.CurrentState()) +} + +func (m *clusterMapper) TransitionTime(eventType privatev1.EventType) (time.Time, error) { + return ResolveTransitionTime(eventType, + m.cl.GetMetadata().GetCreationTimestamp(), + m.cl.GetMetadata().GetDeletionTimestamp(), + m.cl.GetStatus().GetStateTransitionTime(), + m.cl.GetId()) +} + +// IsClusterBillableState returns whether a ClusterOrder state string represents +// a billable state. Single source of truth — used by Watch Consumer, Heartbeat +// Generator, and Reconciler. +func IsClusterBillableState(state string) bool { + return state == ClusterStateProgressing || state == ClusterStateReady +} + +// ClusterBillingDimensions extracts billing dimensions from a Cluster proto, +// including the full component breakdown needed for N+1 decomposition. +// Node sets come from spec (desired state the tenant is billed for), not status. +func ClusterBillingDimensions(cl *privatev1.Cluster) map[string]any { + dims := map[string]any{} + spec := cl.GetSpec() + if spec == nil { + return dims + } + if t := spec.GetTemplate(); t != nil { + dims["cluster_template"] = t.GetName() + } + if vn := spec.GetVersion().GetName(); vn != "" { + dims[DimensionReleaseImage] = vn + } + + // Use []any (not []map[string]any) so DecomposeClusterComponents' type + // assertion works for both fresh dims and JSONB-round-tripped dims. + components := []any{ + map[string]any{ + "node_set": "_control_plane", + "component": "control_plane", + "host_type": "_control_plane", + "node_count": int32(1), + }, + } + + if nodeSets := spec.GetNodeSets(); nodeSets != nil { + keys := make([]string, 0, len(nodeSets)) + for k := range nodeSets { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + ns := nodeSets[k] + components = append(components, map[string]any{ + "node_set": k, + "component": "worker", + "host_type": ns.GetHostType().GetName(), + "node_count": ns.GetSize(), + }) + } + } + + dims["components"] = components + return dims +} + +// ComponentRecord represents one billing record in the N+1 decomposition. +type ComponentRecord struct { + NodeSet string + Component string + HostType string + NodeCount int32 + ClusterTemplate string + ReleaseImage string + IsNew bool +} + +// FlatBillingDimensions returns per-component billing dimensions for a single +// CloudEvent record. +func (cr ComponentRecord) FlatBillingDimensions() map[string]any { + dims := map[string]any{ + "cluster_template": cr.ClusterTemplate, + "node_set": cr.NodeSet, + "component": cr.Component, + "host_type": cr.HostType, + "node_count": cr.NodeCount, + } + if cr.ReleaseImage != "" { + dims[DimensionReleaseImage] = cr.ReleaseImage + } + return dims +} + +// DecomposeClusterComponents extracts N+1 component records from stored +// billing dimensions. Used by Watch Consumer, Heartbeat Generator, and +// Reconciler to fan out one cluster into per-component events. Returns +// ErrDataQuality if the components list itself is missing or malformed — +// per-component node_count is trusted as-is: fulfillment-service's API +// rejects non-positive node set sizes at write time (see +// PrivateClustersServer's node-set-size check), and ClusterBillingDimensions +// always populates node_count from that same validated field, so every +// caller-supplied value is already known good. +func DecomposeClusterComponents(billingDims map[string]any) ([]ComponentRecord, error) { + clusterTemplate, _ := billingDims["cluster_template"].(string) + releaseImage, _ := billingDims[DimensionReleaseImage].(string) + + componentsRaw, ok := billingDims["components"] + if !ok { + return nil, fmt.Errorf("%w: billing dimensions have no components", ErrDataQuality) + } + + components, ok := componentsRaw.([]any) + if !ok { + return nil, fmt.Errorf("%w: billing dimensions components is not a list (got %T)", ErrDataQuality, componentsRaw) + } + + records := make([]ComponentRecord, 0, len(components)) + for _, c := range components { + cm, ok := c.(map[string]any) + if !ok { + continue + } + nodeSet, _ := cm["node_set"].(string) + component, _ := cm["component"].(string) + hostType, _ := cm["host_type"].(string) + + nc, _ := toFloat64(cm["node_count"]) + nodeCount := int32(nc) + + records = append(records, ComponentRecord{ + NodeSet: nodeSet, + Component: component, + HostType: hostType, + NodeCount: nodeCount, + ClusterTemplate: clusterTemplate, + ReleaseImage: releaseImage, + }) + } + + return records, nil +} + +// ComponentEventID derives a deterministic CloudEvent ID for a decomposed +// component event. Uses NodeSet as the unique key per cluster. +func ComponentEventID(baseEventID string, comp ComponentRecord) string { + return fmt.Sprintf("%s/%s", baseEventID, comp.NodeSet) +} + +// DecomposeClusterEvents fans out a single event into N+1 per-component events. +// Returns error if billing dimensions have no components (data quality issue). +// buildFn receives (per-component billing dimensions, deterministic event ID). +func DecomposeClusterEvents(billingDims map[string]any, baseID string, buildFn EventBuilder) ([]cloudevents.Event, error) { + components, err := DecomposeClusterComponents(billingDims) + if err != nil { + return nil, err + } + if len(components) == 0 { + return nil, fmt.Errorf("%w: cluster has no components in billing dimensions", ErrDataQuality) + } + + result := make([]cloudevents.Event, 0, len(components)) + for _, comp := range components { + ce, err := buildFn(comp.FlatBillingDimensions(), ComponentEventID(baseID, comp)) + if err != nil { + return nil, err + } + result = append(result, ce) + } + return result, nil +} + +// ChangedComponents compares old and new billing dimensions and returns +// component records that changed — including newly added or removed node +// sets. "Changed" is defined as "the component's billing-relevant wire +// representation differs" (via FlatBillingDimensions + DimensionsEqual), +// the same equality used to gate entry into this comparison in the first +// place — so this can never miss a field DimensionsEqual would catch. +// Removed components are returned with NodeCount=0. +func ChangedComponents(oldDims, newDims map[string]any) ([]ComponentRecord, error) { + oldRecords, err := DecomposeClusterComponents(oldDims) + if err != nil { + return nil, err + } + newRecords, err := DecomposeClusterComponents(newDims) + if err != nil { + return nil, err + } + + oldByKey := make(map[string]ComponentRecord, len(oldRecords)) + for _, r := range oldRecords { + oldByKey[r.NodeSet] = r + } + + newByKey := make(map[string]bool, len(newRecords)) + var changed []ComponentRecord + for _, r := range newRecords { + newByKey[r.NodeSet] = true + old, exists := oldByKey[r.NodeSet] + if !exists || !DimensionsEqual(old.FlatBillingDimensions(), r.FlatBillingDimensions()) { + r.IsNew = !exists + changed = append(changed, r) + } + } + + for _, r := range oldRecords { + if !newByKey[r.NodeSet] { + changed = append(changed, ComponentRecord{ + NodeSet: r.NodeSet, + Component: r.Component, + HostType: r.HostType, + NodeCount: 0, + ClusterTemplate: r.ClusterTemplate, + ReleaseImage: r.ReleaseImage, + }) + } + } + + return changed, nil +} + +// NextComponentBillableSince returns the per-component "billable since" +// timestamps for newDims: a component unchanged from oldDims carries forward +// its entry from oldSince, a new or changed component resets to now. Callers +// pass oldDims/oldSince as nil to force every current component to reset +// (e.g. a resource newly becoming billable, where any prior per-component +// history is no longer relevant). Resource types that don't decompose into +// components (no "components" key in newDims) return nil. +// +// Reuses ChangedComponents rather than re-implementing the old-vs-new diff, +// so this can never disagree with the changed-component set a caller +// actually publishes for the same transition. +func NextComponentBillableSince(oldDims map[string]any, oldSince map[string]time.Time, newDims map[string]any, now time.Time) map[string]time.Time { + newRecords, err := DecomposeClusterComponents(newDims) + if err != nil { + return nil + } + + // oldDims decomposition failing (e.g. first-ever creation, no prior + // components) just means every current component is treated as changed + // below — correct, since there is no prior state to carry forward. + changed, _ := ChangedComponents(oldDims, newDims) + changedSet := make(map[string]bool, len(changed)) + for _, c := range changed { + changedSet[c.NodeSet] = true + } + + since := make(map[string]time.Time, len(newRecords)) + for _, r := range newRecords { + if !changedSet[r.NodeSet] { + if t, ok := oldSince[r.NodeSet]; ok { + since[r.NodeSet] = t + continue + } + } + since[r.NodeSet] = now + } + return since +} diff --git a/osac-metering/metering-service/internal/events/cluster_test.go b/osac-metering/metering-service/internal/events/cluster_test.go new file mode 100644 index 000000000..73af42820 --- /dev/null +++ b/osac-metering/metering-service/internal/events/cluster_test.go @@ -0,0 +1,1065 @@ +package events_test + +import ( + "encoding/json" + "errors" + "fmt" + + cloudevents "github.com/cloudevents/sdk-go/v2" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "google.golang.org/protobuf/types/known/timestamppb" + + privatev1 "github.com/osac-project/osac-metering/internal/api/osac/private/v1" + "github.com/osac-project/osac-metering/internal/events" +) + +var _ = Describe("CaaS Cluster Mapper", func() { + var cl *privatev1.Cluster + + BeforeEach(func() { + cl = &privatev1.Cluster{ + Id: "cluster-abc-123", + Metadata: &privatev1.Metadata{ + Tenant: "tenant-1", + Project: "project-alpha", + Version: 5, + CreationTimestamp: timestamppb.Now(), + }, + Spec: &privatev1.ClusterSpec{ + Template: &privatev1.ClusterTemplateReference{Id: "ocp-ci-small", Name: "ocp-ci-small"}, + CatalogItem: &privatev1.ClusterCatalogItemReference{Id: "cluster-catalog-1", Name: "cluster-catalog-1"}, + Version: &privatev1.ClusterVersionReference{Id: "4.17.0", Name: "4.17.0"}, + NodeSets: map[string]*privatev1.ClusterNodeSet{ + "gpu-workers": {HostType: &privatev1.HostTypeReference{Name: "gpu-h100"}, Size: 2}, + "cpu-workers": {HostType: &privatev1.HostTypeReference{Name: "cpu-only"}, Size: 3}, + }, + }, + Status: &privatev1.ClusterStatus{ + State: privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, + StateTransitionTime: timestamppb.Now(), + }, + } + }) + + Context("state machine — full transition matrix", func() { + DescribeTable("resolves correct CloudEvent type for state transitions", + func(currentState privatev1.ClusterState, previousState string, expectedType string, expectSkip bool) { + cl.Status.State = currentState + + event := &privatev1.Event{ + Id: "evt-1", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + stateCtx := &events.StateContext{PreviousState: previousState} + ce, err := mapEvent(event, stateCtx) + + if expectSkip { + Expect(err).To(HaveOccurred()) + Expect(errors.Is(err, events.ErrSkipTransition)).To(BeTrue()) + } else { + Expect(err).NotTo(HaveOccurred()) + Expect(ce.Type()).To(Equal(expectedType)) + } + }, + // --- Started: first billable state (no previous) --- + Entry("initial PROGRESSING (prev=empty) -> started.v1", + privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.StateEmpty, events.EventStarted, false), + Entry("initial READY (prev=empty) -> started.v1", + privatev1.ClusterState_CLUSTER_STATE_READY, events.StateEmpty, events.EventStarted, false), + + // --- Skip: first observed in non-billable state --- + Entry("initial FAILED (prev=empty) -> skip", + privatev1.ClusterState_CLUSTER_STATE_FAILED, events.StateEmpty, "", true), + Entry("initial DELETING (prev=empty) -> skip", + privatev1.ClusterState_CLUSTER_STATE_DELETING, events.StateEmpty, "", true), + Entry("initial DELETE_FAILED (prev=empty) -> skip", + privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.StateEmpty, "", true), + Entry("initial UNSPECIFIED (prev=empty) -> skip", + privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.StateEmpty, "", true), + + // --- Resumed: non-billable to billable --- + Entry("FAILED -> PROGRESSING -> resumed.v1", + privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateFailed, events.EventResumed, false), + Entry("FAILED -> READY -> resumed.v1", + privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateFailed, events.EventResumed, false), + Entry("DELETING -> PROGRESSING -> resumed.v1", + privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateDeleting, events.EventResumed, false), + Entry("DELETING -> READY -> resumed.v1", + privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateDeleting, events.EventResumed, false), + Entry("DELETE_FAILED -> PROGRESSING -> resumed.v1", + privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateDeleteFailed, events.EventResumed, false), + Entry("DELETE_FAILED -> READY -> resumed.v1", + privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateDeleteFailed, events.EventResumed, false), + Entry("UNSPECIFIED -> PROGRESSING -> resumed.v1", + privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateUnspecified, events.EventResumed, false), + Entry("UNSPECIFIED -> READY -> resumed.v1", + privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateUnspecified, events.EventResumed, false), + + // --- Suspended: billable to non-billable --- + Entry("PROGRESSING -> FAILED -> suspended.v1", + privatev1.ClusterState_CLUSTER_STATE_FAILED, events.ClusterStateProgressing, events.EventSuspended, false), + Entry("PROGRESSING -> DELETING -> suspended.v1", + privatev1.ClusterState_CLUSTER_STATE_DELETING, events.ClusterStateProgressing, events.EventSuspended, false), + Entry("READY -> FAILED -> suspended.v1", + privatev1.ClusterState_CLUSTER_STATE_FAILED, events.ClusterStateReady, events.EventSuspended, false), + Entry("READY -> DELETING -> suspended.v1", + privatev1.ClusterState_CLUSTER_STATE_DELETING, events.ClusterStateReady, events.EventSuspended, false), + Entry("PROGRESSING -> UNSPECIFIED -> suspended.v1", + privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.ClusterStateProgressing, events.EventSuspended, false), + Entry("READY -> UNSPECIFIED -> suspended.v1", + privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.ClusterStateReady, events.EventSuspended, false), + Entry("READY -> DELETE_FAILED -> suspended.v1", + privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.ClusterStateReady, events.EventSuspended, false), + Entry("PROGRESSING -> DELETE_FAILED -> suspended.v1", + privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.ClusterStateProgressing, events.EventSuspended, false), + + // --- Skip: billable to billable (no billing boundary) --- + Entry("PROGRESSING -> READY -> skip", + privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateProgressing, "", true), + Entry("READY -> PROGRESSING -> skip", + privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateReady, "", true), + Entry("PROGRESSING -> PROGRESSING -> skip (same-state)", + privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, events.ClusterStateProgressing, "", true), + Entry("READY -> READY -> skip (same-state, scaling)", + privatev1.ClusterState_CLUSTER_STATE_READY, events.ClusterStateReady, "", true), + + // --- Skip: non-billable same-state --- + Entry("FAILED -> FAILED -> skip (same-state)", + privatev1.ClusterState_CLUSTER_STATE_FAILED, events.ClusterStateFailed, "", true), + Entry("DELETING -> DELETING -> skip (same-state)", + privatev1.ClusterState_CLUSTER_STATE_DELETING, events.ClusterStateDeleting, "", true), + Entry("DELETE_FAILED -> DELETE_FAILED -> skip (same-state)", + privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.ClusterStateDeleteFailed, "", true), + Entry("UNSPECIFIED -> UNSPECIFIED -> skip (same-state)", + privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.ClusterStateUnspecified, "", true), + + // --- Skip: non-billable to non-billable (cross-state) --- + Entry("FAILED -> DELETING -> skip", + privatev1.ClusterState_CLUSTER_STATE_DELETING, events.ClusterStateFailed, "", true), + Entry("FAILED -> DELETE_FAILED -> skip", + privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.ClusterStateFailed, "", true), + Entry("DELETING -> DELETE_FAILED -> skip", + privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.ClusterStateDeleting, "", true), + Entry("DELETE_FAILED -> DELETING -> skip", + privatev1.ClusterState_CLUSTER_STATE_DELETING, events.ClusterStateDeleteFailed, "", true), + Entry("DELETING -> FAILED -> skip", + privatev1.ClusterState_CLUSTER_STATE_FAILED, events.ClusterStateDeleting, "", true), + Entry("DELETE_FAILED -> FAILED -> skip", + privatev1.ClusterState_CLUSTER_STATE_FAILED, events.ClusterStateDeleteFailed, "", true), + Entry("UNSPECIFIED -> FAILED -> skip", + privatev1.ClusterState_CLUSTER_STATE_FAILED, events.ClusterStateUnspecified, "", true), + Entry("UNSPECIFIED -> DELETING -> skip", + privatev1.ClusterState_CLUSTER_STATE_DELETING, events.ClusterStateUnspecified, "", true), + Entry("UNSPECIFIED -> DELETE_FAILED -> skip", + privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, events.ClusterStateUnspecified, "", true), + Entry("FAILED -> UNSPECIFIED -> skip", + privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.ClusterStateFailed, "", true), + Entry("DELETING -> UNSPECIFIED -> skip", + privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.ClusterStateDeleting, "", true), + Entry("DELETE_FAILED -> UNSPECIFIED -> skip", + privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, events.ClusterStateDeleteFailed, "", true), + ) + + It("returns error for unknown state (missing table entry)", func() { + cl.Status.State = privatev1.ClusterState(9999) + + event := &privatev1.Event{ + Id: "evt-unknown", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + _, err := mapEvent(event, &events.StateContext{PreviousState: events.ClusterStateReady}) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unexpected state transition")) + Expect(errors.Is(err, events.ErrSkipTransition)).To(BeFalse()) + }) + + It("maps OBJECT_CREATED to created.v1", func() { + event := &privatev1.Event{ + Id: "evt-create", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_CREATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + ce, err := mapEvent(event, &events.StateContext{}) + Expect(err).NotTo(HaveOccurred()) + Expect(ce.Type()).To(Equal(events.EventCreated)) + }) + + It("maps OBJECT_DELETED to deleted.v1", func() { + cl.Metadata.DeletionTimestamp = timestamppb.Now() + + event := &privatev1.Event{ + Id: "evt-delete", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_DELETED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + ce, err := mapEvent(event, &events.StateContext{}) + Expect(err).NotTo(HaveOccurred()) + Expect(ce.Type()).To(Equal(events.EventDeleted)) + }) + }) + + Context("resource mapper fields", func() { + It("returns resource_type=cluster_order", func() { + event := &privatev1.Event{ + Id: "evt-1", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_CREATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + ce, err := mapEvent(event, &events.StateContext{}) + Expect(err).NotTo(HaveOccurred()) + + var data map[string]any + Expect(json.Unmarshal(ce.Data(), &data)).To(Succeed()) + Expect(data["resource_type"]).To(Equal(events.ResourceTypeClusterOrder)) + }) + + It("extracts resource_id from cluster ID", func() { + event := &privatev1.Event{ + Id: "evt-1", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_CREATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + ce, err := mapEvent(event, &events.StateContext{}) + Expect(err).NotTo(HaveOccurred()) + Expect(ce.Extensions()["osacresourceid"]).To(Equal("cluster-abc-123")) + }) + + It("extracts tenant_id, project_id, template_id, catalog_item_id from metadata and spec", func() { + event := &privatev1.Event{ + Id: "evt-1", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_CREATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + ce, err := mapEvent(event, &events.StateContext{}) + Expect(err).NotTo(HaveOccurred()) + + var data map[string]any + Expect(json.Unmarshal(ce.Data(), &data)).To(Succeed()) + Expect(data["tenant_id"]).To(Equal("tenant-1")) + Expect(data["project_id"]).To(Equal("project-alpha")) + Expect(data["template_id"]).To(Equal("ocp-ci-small")) + Expect(data["catalog_item_id"]).To(Equal("cluster-catalog-1")) + }) + + It("trims CLUSTER_STATE_ prefix from current_state", func() { + cl.Status.State = privatev1.ClusterState_CLUSTER_STATE_READY + event := &privatev1.Event{ + Id: "evt-1", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_CREATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + ce, err := mapEvent(event, &events.StateContext{}) + Expect(err).NotTo(HaveOccurred()) + + var data map[string]any + Expect(json.Unmarshal(ce.Data(), &data)).To(Succeed()) + Expect(data["current_state"]).To(Equal(events.ClusterStateReady)) + }) + }) + + Context("billability", func() { + It("PROGRESSING is billable", func() { + Expect(events.IsClusterBillableState(events.ClusterStateProgressing)).To(BeTrue()) + }) + + It("READY is billable", func() { + Expect(events.IsClusterBillableState(events.ClusterStateReady)).To(BeTrue()) + }) + + It("FAILED is not billable", func() { + Expect(events.IsClusterBillableState(events.ClusterStateFailed)).To(BeFalse()) + }) + + It("DELETING is not billable", func() { + Expect(events.IsClusterBillableState(events.ClusterStateDeleting)).To(BeFalse()) + }) + + It("DELETE_FAILED is not billable", func() { + Expect(events.IsClusterBillableState(events.ClusterStateDeleteFailed)).To(BeFalse()) + }) + + It("UNSPECIFIED is not billable", func() { + Expect(events.IsClusterBillableState(events.ClusterStateUnspecified)).To(BeFalse()) + }) + }) + + Context("billing dimensions", func() { + It("includes cluster_template, release_image, and full components breakdown", func() { + dims := events.ClusterBillingDimensions(cl) + Expect(dims["cluster_template"]).To(Equal("ocp-ci-small")) + Expect(dims["release_image"]).To(Equal("4.17.0")) + + components, ok := dims["components"].([]any) + Expect(ok).To(BeTrue(), "components must be []any for DecomposeClusterComponents compatibility") + Expect(components).To(HaveLen(3)) + + cp := components[0].(map[string]any) + Expect(cp["node_set"]).To(Equal("_control_plane")) + Expect(cp["component"]).To(Equal("control_plane")) + Expect(cp["host_type"]).To(Equal("_control_plane")) + Expect(cp["node_count"]).To(Equal(int32(1))) + }) + + It("sorts worker node sets by key for deterministic ordering", func() { + dims := events.ClusterBillingDimensions(cl) + components := dims["components"].([]any) + + // control_plane first, then sorted by node set key: "cpu-workers" < "gpu-workers" + w1 := components[1].(map[string]any) + Expect(w1["node_set"]).To(Equal("cpu-workers")) + Expect(w1["host_type"]).To(Equal("cpu-only")) + Expect(w1["node_count"]).To(Equal(int32(3))) + w2 := components[2].(map[string]any) + Expect(w2["node_set"]).To(Equal("gpu-workers")) + Expect(w2["host_type"]).To(Equal("gpu-h100")) + Expect(w2["node_count"]).To(Equal(int32(2))) + }) + + It("omits release_image when nil", func() { + cl.Spec.Version = nil + dims := events.ClusterBillingDimensions(cl) + Expect(dims).NotTo(HaveKey("release_image")) + }) + + It("handles nil spec gracefully", func() { + cl.Spec = nil + dims := events.ClusterBillingDimensions(cl) + Expect(dims).To(BeEmpty()) + }) + + It("handles nil node_sets with control plane only", func() { + cl.Spec.NodeSets = nil + dims := events.ClusterBillingDimensions(cl) + components := dims["components"].([]any) + Expect(components).To(HaveLen(1)) + cp := components[0].(map[string]any) + Expect(cp["node_set"]).To(Equal("_control_plane")) + Expect(cp["component"]).To(Equal("control_plane")) + }) + + It("DecomposeClusterComponents works on fresh (non-JSONB) output", func() { + dims := events.ClusterBillingDimensions(cl) + records, err := events.DecomposeClusterComponents(dims) + Expect(err).NotTo(HaveOccurred()) + Expect(records).To(HaveLen(3)) + Expect(records[0].NodeSet).To(Equal("_control_plane")) + Expect(records[0].Component).To(Equal("control_plane")) + Expect(records[0].NodeCount).To(Equal(int32(1))) + Expect(records[1].NodeSet).To(Equal("cpu-workers")) + Expect(records[1].Component).To(Equal("worker")) + Expect(records[2].NodeSet).To(Equal("gpu-workers")) + Expect(records[2].Component).To(Equal("worker")) + }) + }) + + Context("transition time", func() { + It("uses creation_timestamp for CREATED events", func() { + event := &privatev1.Event{ + Id: "evt-1", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_CREATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + ce, err := mapEvent(event, &events.StateContext{}) + Expect(err).NotTo(HaveOccurred()) + Expect(ce.Time()).To(Equal(cl.Metadata.CreationTimestamp.AsTime())) + }) + + It("uses state_transition_time for UPDATED events", func() { + event := &privatev1.Event{ + Id: "evt-1", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + ce, err := mapEvent(event, &events.StateContext{}) + Expect(err).NotTo(HaveOccurred()) + Expect(ce.Time()).To(Equal(cl.Status.StateTransitionTime.AsTime())) + }) + + It("rejects CREATED without creation_timestamp", func() { + cl.Metadata.CreationTimestamp = nil + event := &privatev1.Event{ + Id: "evt-1", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_CREATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + _, err := mapEvent(event, &events.StateContext{}) + Expect(err).To(HaveOccurred()) + Expect(errors.Is(err, events.ErrDataQuality)).To(BeTrue()) + }) + + It("rejects UPDATED without state_transition_time", func() { + cl.Status.StateTransitionTime = nil + event := &privatev1.Event{ + Id: "evt-1", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + _, err := mapEvent(event, &events.StateContext{}) + Expect(err).To(HaveOccurred()) + Expect(errors.Is(err, events.ErrDataQuality)).To(BeTrue()) + }) + }) + + Context("error handling", func() { + It("rejects events with empty resource_id", func() { + cl.Id = "" + event := &privatev1.Event{ + Id: "evt-1", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_CREATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + _, err := mapEvent(event, &events.StateContext{}) + Expect(err).To(HaveOccurred()) + Expect(errors.Is(err, events.ErrDataQuality)).To(BeTrue()) + }) + + It("rejects events with empty tenant_id", func() { + cl.Metadata.Tenant = "" + event := &privatev1.Event{ + Id: "evt-1", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_CREATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + _, err := mapEvent(event, &events.StateContext{}) + Expect(err).To(HaveOccurred()) + Expect(errors.Is(err, events.ErrDataQuality)).To(BeTrue()) + }) + }) +}) + +var _ = Describe("DecomposeClusterEvents", func() { + It("produces N+1 events with per-component dims and deterministic IDs", func() { + dims := map[string]any{ + "cluster_template": "ocp-ci-small", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + + built := []string{} + result, err := events.DecomposeClusterEvents(dims, "base-id", func(d map[string]any, eventID string) (cloudevents.Event, error) { + built = append(built, eventID) + ce := cloudevents.NewEvent() + ce.SetID(eventID) + Expect(ce.SetData(cloudevents.ApplicationJSON, d)).To(Succeed()) + return ce, nil + }) + + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(HaveLen(2)) + Expect(built).To(ConsistOf("base-id/_control_plane", "base-id/gpu-workers")) + }) + + It("returns ErrDataQuality when cluster has no components", func() { + dims := map[string]any{"cluster_template": "ocp-ci-small"} + + _, err := events.DecomposeClusterEvents(dims, "base-id", func(d map[string]any, eventID string) (cloudevents.Event, error) { + Fail("buildFn should not be called") + return cloudevents.Event{}, nil + }) + + Expect(err).To(HaveOccurred()) + Expect(errors.Is(err, events.ErrDataQuality)).To(BeTrue()) + }) + + It("returns ErrDataQuality when components array is empty", func() { + dims := map[string]any{ + "cluster_template": "ocp-ci-small", + "components": []any{}, + } + + _, err := events.DecomposeClusterEvents(dims, "base-id", func(d map[string]any, eventID string) (cloudevents.Event, error) { + Fail("buildFn should not be called") + return cloudevents.Event{}, nil + }) + + Expect(err).To(HaveOccurred()) + Expect(errors.Is(err, events.ErrDataQuality)).To(BeTrue()) + }) + + It("propagates buildFn errors", func() { + dims := map[string]any{ + "components": []any{ + map[string]any{"node_set": "_control_plane", "node_count": int32(1)}, + }, + } + + _, err := events.DecomposeClusterEvents(dims, "base-id", func(d map[string]any, eventID string) (cloudevents.Event, error) { + return cloudevents.Event{}, fmt.Errorf("kafka down") + }) + + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("kafka down")) + }) +}) + +var _ = Describe("DecomposeClusterComponents", func() { + It("decomposes 1 control plane + 2 worker sets into 3 records", func() { + dims := map[string]any{ + "cluster_template": "ocp-ci-small", + "release_image": "quay.io/ocp:4.17.0", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + map[string]any{"node_set": "cpu-workers", "component": "worker", "host_type": "cpu-only", "node_count": int32(3)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + + records, err := events.DecomposeClusterComponents(dims) + Expect(err).NotTo(HaveOccurred()) + Expect(records).To(HaveLen(3)) + + Expect(records[0].NodeSet).To(Equal("_control_plane")) + Expect(records[0].Component).To(Equal("control_plane")) + Expect(records[0].HostType).To(Equal("_control_plane")) + Expect(records[0].NodeCount).To(Equal(int32(1))) + Expect(records[0].ClusterTemplate).To(Equal("ocp-ci-small")) + Expect(records[0].ReleaseImage).To(Equal("quay.io/ocp:4.17.0")) + + Expect(records[1].NodeSet).To(Equal("cpu-workers")) + Expect(records[1].Component).To(Equal("worker")) + Expect(records[1].HostType).To(Equal("cpu-only")) + Expect(records[1].NodeCount).To(Equal(int32(3))) + + Expect(records[2].NodeSet).To(Equal("gpu-workers")) + Expect(records[2].Component).To(Equal("worker")) + Expect(records[2].HostType).To(Equal("gpu-h100")) + Expect(records[2].NodeCount).To(Equal(int32(2))) + }) + + It("handles node_count as float64 (JSONB round-trip)", func() { + dims := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": float64(1)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": float64(2)}, + }, + } + + records, err := events.DecomposeClusterComponents(dims) + Expect(err).NotTo(HaveOccurred()) + Expect(records).To(HaveLen(2)) + Expect(records[0].NodeCount).To(Equal(int32(1))) + Expect(records[1].NodeCount).To(Equal(int32(2))) + }) + + It("returns ErrDataQuality when no components key", func() { + dims := map[string]any{"cluster_template": "tmpl"} + _, err := events.DecomposeClusterComponents(dims) + Expect(errors.Is(err, events.ErrDataQuality)).To(BeTrue()) + }) + + It("returns ErrDataQuality for empty dims", func() { + _, err := events.DecomposeClusterComponents(map[string]any{}) + Expect(errors.Is(err, events.ErrDataQuality)).To(BeTrue()) + }) + +}) + +var _ = Describe("ComponentRecord", func() { + It("produces flat billing dimensions", func() { + cr := events.ComponentRecord{ + NodeSet: "gpu-workers", + Component: "worker", + HostType: "gpu-h100", + NodeCount: 2, + ClusterTemplate: "ocp-ci-small", + ReleaseImage: "quay.io/ocp:4.17.0", + } + + flat := cr.FlatBillingDimensions() + Expect(flat["cluster_template"]).To(Equal("ocp-ci-small")) + Expect(flat["release_image"]).To(Equal("quay.io/ocp:4.17.0")) + Expect(flat["node_set"]).To(Equal("gpu-workers")) + Expect(flat["component"]).To(Equal("worker")) + Expect(flat["host_type"]).To(Equal("gpu-h100")) + Expect(flat["node_count"]).To(Equal(int32(2))) + }) + + It("omits release_image when empty", func() { + cr := events.ComponentRecord{ + NodeSet: "_control_plane", + Component: "control_plane", + HostType: "_control_plane", + NodeCount: 1, + ClusterTemplate: "tmpl", + } + + flat := cr.FlatBillingDimensions() + Expect(flat).NotTo(HaveKey("release_image")) + }) +}) + +var _ = Describe("ComponentEventID", func() { + It("produces deterministic IDs", func() { + comp := events.ComponentRecord{NodeSet: "gpu-workers", Component: "worker", HostType: "gpu-h100"} + id1 := events.ComponentEventID("evt-123", comp) + id2 := events.ComponentEventID("evt-123", comp) + Expect(id1).To(Equal(id2)) + Expect(id1).To(Equal("evt-123/gpu-workers")) + }) + + It("produces different IDs for different components", func() { + cp := events.ComponentRecord{NodeSet: "_control_plane", Component: "control_plane", HostType: "_control_plane"} + worker := events.ComponentRecord{NodeSet: "gpu-workers", Component: "worker", HostType: "gpu-h100"} + Expect(events.ComponentEventID("evt-1", cp)).NotTo(Equal(events.ComponentEventID("evt-1", worker))) + }) + + It("produces different IDs for different base events", func() { + comp := events.ComponentRecord{NodeSet: "gpu-workers", Component: "worker", HostType: "gpu-h100"} + Expect(events.ComponentEventID("evt-1", comp)).NotTo(Equal(events.ComponentEventID("evt-2", comp))) + }) +}) + +var _ = Describe("ChangedComponents", func() { + It("detects node_count change in a single worker set", func() { + oldDims := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + newDims := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(4)}, + }, + } + + changed, err := events.ChangedComponents(oldDims, newDims) + Expect(err).NotTo(HaveOccurred()) + Expect(changed).To(HaveLen(1)) + Expect(changed[0].HostType).To(Equal("gpu-h100")) + Expect(changed[0].NodeCount).To(Equal(int32(4))) + }) + + It("returns empty when nothing changed", func() { + dims := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + }, + } + + changed, err := events.ChangedComponents(dims, dims) + Expect(err).NotTo(HaveOccurred()) + Expect(changed).To(BeEmpty()) + }) + + It("detects newly added worker node set", func() { + oldDims := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + }, + } + newDims := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + + changed, err := events.ChangedComponents(oldDims, newDims) + Expect(err).NotTo(HaveOccurred()) + Expect(changed).To(HaveLen(1)) + Expect(changed[0].HostType).To(Equal("gpu-h100")) + }) + + It("detects removed worker node set with NodeCount=0", func() { + oldDims := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + newDims := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + }, + } + + changed, err := events.ChangedComponents(oldDims, newDims) + Expect(err).NotTo(HaveOccurred()) + Expect(changed).To(HaveLen(1)) + Expect(changed[0].HostType).To(Equal("gpu-h100")) + Expect(changed[0].NodeCount).To(Equal(int32(0))) + Expect(changed[0].NodeSet).To(Equal("gpu-workers")) + }) + + It("preserves distinct NodeSet on multi-removal for unique event IDs", func() { + oldDims := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + map[string]any{"node_set": "pool-a", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + map[string]any{"node_set": "pool-b", "component": "worker", "host_type": "cpu-only", "node_count": int32(3)}, + }, + } + newDims := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + }, + } + + changed, err := events.ChangedComponents(oldDims, newDims) + Expect(err).NotTo(HaveOccurred()) + Expect(changed).To(HaveLen(2)) + + nodeSetToHostType := map[string]string{} + for _, c := range changed { + Expect(c.NodeCount).To(Equal(int32(0))) + Expect(c.NodeSet).NotTo(BeEmpty()) + id := events.ComponentEventID("evt-1", c) + Expect(id).NotTo(Equal("evt-1/")) + nodeSetToHostType[c.NodeSet] = c.HostType + } + Expect(nodeSetToHostType).To(HaveLen(2)) + Expect(nodeSetToHostType["pool-a"]).To(Equal("gpu-h100")) + Expect(nodeSetToHostType["pool-b"]).To(Equal("cpu-only")) + }) + + It("sets IsNew=true for newly-added components and IsNew=false for modified", func() { + oldDims := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + newDims := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(4)}, + map[string]any{"node_set": "tpu-workers", "component": "worker", "host_type": "tpu-v5", "node_count": int32(2)}, + }, + } + + changed, err := events.ChangedComponents(oldDims, newDims) + Expect(err).NotTo(HaveOccurred()) + Expect(changed).To(HaveLen(2)) + + byNodeSet := map[string]events.ComponentRecord{} + for _, c := range changed { + byNodeSet[c.NodeSet] = c + } + + Expect(byNodeSet["gpu-workers"].IsNew).To(BeFalse(), + "modified component should have IsNew=false") + Expect(byNodeSet["tpu-workers"].IsNew).To(BeTrue(), + "newly-added component should have IsNew=true") + }) + + It("detects host_type change within a node set", func() { + oldDims := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + newDims := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-a100", "node_count": int32(2)}, + }, + } + + changed, err := events.ChangedComponents(oldDims, newDims) + Expect(err).NotTo(HaveOccurred()) + Expect(changed).To(HaveLen(1)) + Expect(changed[0].HostType).To(Equal("gpu-a100")) + Expect(changed[0].NodeCount).To(Equal(int32(2))) + }) + + It("detects cluster_template change with unchanged node_count and host_type", func() { + oldDims := map[string]any{ + "cluster_template": "ocp-ci-small", + "components": []any{ + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + newDims := map[string]any{ + "cluster_template": "ocp-ci-large", + "components": []any{ + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + + changed, err := events.ChangedComponents(oldDims, newDims) + Expect(err).NotTo(HaveOccurred()) + Expect(changed).To(HaveLen(1), "a cluster_template-only change must still be reported so it can be published") + Expect(changed[0].ClusterTemplate).To(Equal("ocp-ci-large")) + }) + + It("detects release_image change with unchanged node_count and host_type", func() { + oldDims := map[string]any{ + "cluster_template": "tmpl", + "release_image": "quay.io/openshift-release-dev/ocp-release:4.17.0-x86_64", + "components": []any{ + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + newDims := map[string]any{ + "cluster_template": "tmpl", + "release_image": "quay.io/openshift-release-dev/ocp-release:4.18.0-x86_64", + "components": []any{ + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + + changed, err := events.ChangedComponents(oldDims, newDims) + Expect(err).NotTo(HaveOccurred()) + Expect(changed).To(HaveLen(1), "a cluster upgrade (release_image-only change) must still be reported so it can be published") + Expect(changed[0].ReleaseImage).To(Equal("quay.io/openshift-release-dev/ocp-release:4.18.0-x86_64")) + }) + + It("handles int32 vs float64 from JSONB round-trip", func() { + oldDims := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + newDims := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": float64(2)}, + }, + } + + changed, err := events.ChangedComponents(oldDims, newDims) + Expect(err).NotTo(HaveOccurred()) + Expect(changed).To(BeEmpty()) + }) +}) + +var _ = Describe("DimensionsEqual with nested CaaS components", func() { + It("matches identical billing dimensions with components array", func() { + a := map[string]any{ + "cluster_template": "ocp-ci-small", + "release_image": "quay.io/ocp:4.17.0", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + b := map[string]any{ + "cluster_template": "ocp-ci-small", + "release_image": "quay.io/ocp:4.17.0", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + Expect(events.DimensionsEqual(a, b)).To(BeTrue()) + }) + + It("detects node_count change in components array", func() { + a := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + b := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(4)}, + }, + } + Expect(events.DimensionsEqual(a, b)).To(BeFalse()) + }) + + It("detects different number of components", func() { + a := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + }, + } + b := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + Expect(events.DimensionsEqual(a, b)).To(BeFalse()) + }) + + It("handles JSONB round-trip: int32 stored, float64 on read", func() { + stored := map[string]any{ + "cluster_template": "tmpl", + "release_image": "quay.io/ocp:4.17.0", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + + // Simulate JSONB round-trip: marshal then unmarshal + data, err := json.Marshal(stored) + Expect(err).NotTo(HaveOccurred()) + + var roundTripped map[string]any + Expect(json.Unmarshal(data, &roundTripped)).To(Succeed()) + + // After JSON round-trip: int32 becomes float64, []map becomes []any + Expect(events.DimensionsEqual(stored, roundTripped)).To(BeTrue()) + }) + + It("detects node_count change after JSONB round-trip", func() { + stored := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + data, err := json.Marshal(stored) + Expect(err).NotTo(HaveOccurred()) + + var roundTripped map[string]any + Expect(json.Unmarshal(data, &roundTripped)).To(Succeed()) + + // Change node_count in the incoming (non-round-tripped) version + incoming := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(4)}, + }, + } + + Expect(events.DimensionsEqual(roundTripped, incoming)).To(BeFalse()) + }) + + It("round-trip preserves equality for multi-component clusters", func() { + original := map[string]any{ + "cluster_template": "ocp-ci-small", + "release_image": "quay.io/ocp:4.17.0", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + map[string]any{"node_set": "cpu-workers", "component": "worker", "host_type": "cpu-only", "node_count": int32(3)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + + data, err := json.Marshal(original) + Expect(err).NotTo(HaveOccurred()) + var rt1 map[string]any + Expect(json.Unmarshal(data, &rt1)).To(Succeed()) + + // Round-trip again to simulate double-read + data2, err := json.Marshal(rt1) + Expect(err).NotTo(HaveOccurred()) + var rt2 map[string]any + Expect(json.Unmarshal(data2, &rt2)).To(Succeed()) + + Expect(events.DimensionsEqual(rt1, rt2)).To(BeTrue()) + }) + + It("detects host_type change within components", func() { + a := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + b := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-a100", "node_count": int32(2)}, + }, + } + Expect(events.DimensionsEqual(a, b)).To(BeFalse()) + }) + + It("treats different component array order as unequal", func() { + a := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "_control_plane", "node_count": int32(1)}, + map[string]any{"node_set": "gpu-workers", "node_count": int32(2)}, + }, + } + b := map[string]any{ + "cluster_template": "tmpl", + "components": []any{ + map[string]any{"node_set": "gpu-workers", "node_count": int32(2)}, + map[string]any{"node_set": "_control_plane", "node_count": int32(1)}, + }, + } + Expect(events.DimensionsEqual(a, b)).To(BeFalse(), + "component array order matters — ClusterBillingDimensions sorts keys for deterministic ordering") + }) +}) + +var _ = Describe("CaaS transition table completeness", func() { + stateProtoMap := map[string]privatev1.ClusterState{ + events.ClusterStateProgressing: privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, + events.ClusterStateReady: privatev1.ClusterState_CLUSTER_STATE_READY, + events.ClusterStateFailed: privatev1.ClusterState_CLUSTER_STATE_FAILED, + events.ClusterStateDeleting: privatev1.ClusterState_CLUSTER_STATE_DELETING, + events.ClusterStateDeleteFailed: privatev1.ClusterState_CLUSTER_STATE_DELETE_FAILED, + events.ClusterStateUnspecified: privatev1.ClusterState_CLUSTER_STATE_UNSPECIFIED, + } + + It("covers every (from, to) state pair from all proto states plus empty initial", func() { + fromStates := []string{events.StateEmpty, events.ClusterStateProgressing, events.ClusterStateReady, events.ClusterStateFailed, events.ClusterStateDeleting, events.ClusterStateDeleteFailed, events.ClusterStateUnspecified} + toStates := []string{events.ClusterStateProgressing, events.ClusterStateReady, events.ClusterStateFailed, events.ClusterStateDeleting, events.ClusterStateDeleteFailed, events.ClusterStateUnspecified} + + for _, from := range fromStates { + for _, to := range toStates { + cl := &privatev1.Cluster{ + Id: "cl-completeness", + Metadata: &privatev1.Metadata{Tenant: "t", CreationTimestamp: timestamppb.Now()}, + Spec: &privatev1.ClusterSpec{}, + Status: &privatev1.ClusterStatus{ + State: stateProtoMap[to], + StateTransitionTime: timestamppb.Now(), + }, + } + + event := &privatev1.Event{ + Id: "evt-completeness", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + stateCtx := &events.StateContext{PreviousState: from} + _, err := mapEvent(event, stateCtx) + + Expect(err == nil || + errors.Is(err, events.ErrSkipTransition) || + errors.Is(err, events.ErrTransientState)).To(BeTrue(), + "transition %s -> %s returned unexpected error: %v", from, to, err) + } + } + }) +}) diff --git a/osac-metering/metering-service/internal/events/compute_instance.go b/osac-metering/metering-service/internal/events/compute_instance.go index 810053fea..12a736d6c 100644 --- a/osac-metering/metering-service/internal/events/compute_instance.go +++ b/osac-metering/metering-service/internal/events/compute_instance.go @@ -1,7 +1,6 @@ package events import ( - "fmt" "strings" "time" @@ -10,11 +9,121 @@ import ( const ComputeInstanceStatePrefix = "COMPUTE_INSTANCE_STATE_" +// VMaaS compute instance state constants. +const ( + ComputeInstanceStateRunning = "RUNNING" + ComputeInstanceStateStopped = "STOPPED" + ComputeInstanceStatePaused = "PAUSED" + ComputeInstanceStateFailed = "FAILED" + ComputeInstanceStateStopping = "STOPPING" + ComputeInstanceStateStarting = "STARTING" + ComputeInstanceStateDeleting = "DELETING" + ComputeInstanceStateUnspecified = "UNSPECIFIED" +) + +// Compute instance state machine. Every (from, to) pair is enumerated +// explicitly — no wildcards. Missing entry = error (fail fast). +// +// Billable: RUNNING +// Non-billable: STOPPED, PAUSED, FAILED, DELETING, UNSPECIFIED +// Transient: STOPPING, STARTING +var computeInstanceTransitions = TransitionTable{ + // --- From "" (initial observation) --- + {StateEmpty, ComputeInstanceStateRunning}: {EventType: EventStarted}, + {StateEmpty, ComputeInstanceStateStopped}: {Skip: true}, + {StateEmpty, ComputeInstanceStatePaused}: {Skip: true}, + {StateEmpty, ComputeInstanceStateFailed}: {Skip: true}, + {StateEmpty, ComputeInstanceStateStopping}: {Transient: true}, + {StateEmpty, ComputeInstanceStateStarting}: {Transient: true}, + {StateEmpty, ComputeInstanceStateDeleting}: {Skip: true}, + {StateEmpty, ComputeInstanceStateUnspecified}: {Skip: true}, + + // --- From RUNNING --- + {ComputeInstanceStateRunning, ComputeInstanceStateRunning}: {Skip: true}, + {ComputeInstanceStateRunning, ComputeInstanceStateStopped}: {EventType: EventSuspended}, + {ComputeInstanceStateRunning, ComputeInstanceStatePaused}: {EventType: EventSuspended}, + {ComputeInstanceStateRunning, ComputeInstanceStateFailed}: {EventType: EventSuspended}, + {ComputeInstanceStateRunning, ComputeInstanceStateStopping}: {Transient: true}, + {ComputeInstanceStateRunning, ComputeInstanceStateStarting}: {Transient: true}, + {ComputeInstanceStateRunning, ComputeInstanceStateDeleting}: {EventType: EventSuspended}, + {ComputeInstanceStateRunning, ComputeInstanceStateUnspecified}: {EventType: EventSuspended}, + + // --- From STOPPED --- + {ComputeInstanceStateStopped, ComputeInstanceStateRunning}: {EventType: EventResumed}, + {ComputeInstanceStateStopped, ComputeInstanceStateStopped}: {Skip: true}, + {ComputeInstanceStateStopped, ComputeInstanceStatePaused}: {Skip: true}, + {ComputeInstanceStateStopped, ComputeInstanceStateFailed}: {Skip: true}, + {ComputeInstanceStateStopped, ComputeInstanceStateStopping}: {Transient: true}, + {ComputeInstanceStateStopped, ComputeInstanceStateStarting}: {Transient: true}, + {ComputeInstanceStateStopped, ComputeInstanceStateDeleting}: {Skip: true}, + {ComputeInstanceStateStopped, ComputeInstanceStateUnspecified}: {Skip: true}, + + // --- From PAUSED --- + {ComputeInstanceStatePaused, ComputeInstanceStateRunning}: {EventType: EventResumed}, + {ComputeInstanceStatePaused, ComputeInstanceStateStopped}: {Skip: true}, + {ComputeInstanceStatePaused, ComputeInstanceStatePaused}: {Skip: true}, + {ComputeInstanceStatePaused, ComputeInstanceStateFailed}: {Skip: true}, + {ComputeInstanceStatePaused, ComputeInstanceStateStopping}: {Transient: true}, + {ComputeInstanceStatePaused, ComputeInstanceStateStarting}: {Transient: true}, + {ComputeInstanceStatePaused, ComputeInstanceStateDeleting}: {Skip: true}, + {ComputeInstanceStatePaused, ComputeInstanceStateUnspecified}: {Skip: true}, + + // --- From FAILED --- + {ComputeInstanceStateFailed, ComputeInstanceStateRunning}: {EventType: EventResumed}, + {ComputeInstanceStateFailed, ComputeInstanceStateStopped}: {Skip: true}, + {ComputeInstanceStateFailed, ComputeInstanceStatePaused}: {Skip: true}, + {ComputeInstanceStateFailed, ComputeInstanceStateFailed}: {Skip: true}, + {ComputeInstanceStateFailed, ComputeInstanceStateStopping}: {Transient: true}, + {ComputeInstanceStateFailed, ComputeInstanceStateStarting}: {Transient: true}, + {ComputeInstanceStateFailed, ComputeInstanceStateDeleting}: {Skip: true}, + {ComputeInstanceStateFailed, ComputeInstanceStateUnspecified}: {Skip: true}, + + // --- From STOPPING --- + {ComputeInstanceStateStopping, ComputeInstanceStateRunning}: {EventType: EventResumed}, + {ComputeInstanceStateStopping, ComputeInstanceStateStopped}: {Skip: true}, + {ComputeInstanceStateStopping, ComputeInstanceStatePaused}: {Skip: true}, + {ComputeInstanceStateStopping, ComputeInstanceStateFailed}: {Skip: true}, + {ComputeInstanceStateStopping, ComputeInstanceStateStopping}: {Skip: true}, + {ComputeInstanceStateStopping, ComputeInstanceStateStarting}: {Transient: true}, + {ComputeInstanceStateStopping, ComputeInstanceStateDeleting}: {Skip: true}, + {ComputeInstanceStateStopping, ComputeInstanceStateUnspecified}: {Skip: true}, + + // --- From STARTING --- + {ComputeInstanceStateStarting, ComputeInstanceStateRunning}: {EventType: EventResumed}, + {ComputeInstanceStateStarting, ComputeInstanceStateStopped}: {Skip: true}, + {ComputeInstanceStateStarting, ComputeInstanceStatePaused}: {Skip: true}, + {ComputeInstanceStateStarting, ComputeInstanceStateFailed}: {Skip: true}, + {ComputeInstanceStateStarting, ComputeInstanceStateStopping}: {Transient: true}, + {ComputeInstanceStateStarting, ComputeInstanceStateStarting}: {Skip: true}, + {ComputeInstanceStateStarting, ComputeInstanceStateDeleting}: {Skip: true}, + {ComputeInstanceStateStarting, ComputeInstanceStateUnspecified}: {Skip: true}, + + // --- From DELETING --- + {ComputeInstanceStateDeleting, ComputeInstanceStateRunning}: {EventType: EventResumed}, + {ComputeInstanceStateDeleting, ComputeInstanceStateStopped}: {Skip: true}, + {ComputeInstanceStateDeleting, ComputeInstanceStatePaused}: {Skip: true}, + {ComputeInstanceStateDeleting, ComputeInstanceStateFailed}: {Skip: true}, + {ComputeInstanceStateDeleting, ComputeInstanceStateStopping}: {Transient: true}, + {ComputeInstanceStateDeleting, ComputeInstanceStateStarting}: {Transient: true}, + {ComputeInstanceStateDeleting, ComputeInstanceStateDeleting}: {Skip: true}, + {ComputeInstanceStateDeleting, ComputeInstanceStateUnspecified}: {Skip: true}, + + // --- From UNSPECIFIED --- + {ComputeInstanceStateUnspecified, ComputeInstanceStateRunning}: {EventType: EventResumed}, + {ComputeInstanceStateUnspecified, ComputeInstanceStateStopped}: {Skip: true}, + {ComputeInstanceStateUnspecified, ComputeInstanceStatePaused}: {Skip: true}, + {ComputeInstanceStateUnspecified, ComputeInstanceStateFailed}: {Skip: true}, + {ComputeInstanceStateUnspecified, ComputeInstanceStateStopping}: {Transient: true}, + {ComputeInstanceStateUnspecified, ComputeInstanceStateStarting}: {Transient: true}, + {ComputeInstanceStateUnspecified, ComputeInstanceStateDeleting}: {Skip: true}, + {ComputeInstanceStateUnspecified, ComputeInstanceStateUnspecified}: {Skip: true}, +} + type computeInstanceMapper struct { ci *privatev1.ComputeInstance } -func (m *computeInstanceMapper) ResourceType() string { return "compute_instance" } +func (m *computeInstanceMapper) ResourceType() string { return ResourceTypeComputeInstance } func (m *computeInstanceMapper) ResourceID() string { return m.ci.GetId() } func (m *computeInstanceMapper) FulfillmentVersion() int32 { @@ -57,7 +166,7 @@ func ComputeInstanceBillingDimensions(ci *privatev1.ComputeInstance) map[string] // a billable state. Single source of truth for billability — used by both // the Watch Consumer (via IsBillable) and the Reconciler. func IsBillableState(state string) bool { - return state == "RUNNING" + return state == ComputeInstanceStateRunning } func (m *computeInstanceMapper) TenantID() string { @@ -101,61 +210,13 @@ func (m *computeInstanceMapper) CurrentState() string { } func (m *computeInstanceMapper) CloudEventType(eventType privatev1.EventType, previousState string) (string, error) { - switch eventType { - case privatev1.EventType_EVENT_TYPE_OBJECT_CREATED: - return "osac.resource.created.v1", nil - case privatev1.EventType_EVENT_TYPE_OBJECT_DELETED: - return "osac.resource.deleted.v1", nil - case privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED: - return m.resolveUpdatedEventType(previousState) - default: - return "", fmt.Errorf("unsupported event type: %v", eventType) - } -} - -func (m *computeInstanceMapper) resolveUpdatedEventType(previousState string) (string, error) { - currentState := m.CurrentState() - - switch { - case currentState == "RUNNING" && (previousState == "STOPPED" || previousState == "PAUSED"): - return "osac.resource.resumed.v1", nil - case currentState == "RUNNING": - return "osac.resource.started.v1", nil - case currentState == "STOPPED" || currentState == "PAUSED" || currentState == "FAILED" || currentState == "DELETING": - return "osac.resource.suspended.v1", nil - case currentState == "STOPPING" || currentState == "STARTING": - return "", ErrTransientState - case currentState == "UNSPECIFIED": - return "osac.resource.updated.v1", nil - default: - return "", fmt.Errorf("unexpected compute instance state transition: %s -> %s", previousState, currentState) - } + return ResolveCloudEventType(computeInstanceTransitions, eventType, previousState, m.CurrentState()) } func (m *computeInstanceMapper) TransitionTime(eventType privatev1.EventType) (time.Time, error) { - switch eventType { - case privatev1.EventType_EVENT_TYPE_OBJECT_CREATED: - if md := m.ci.GetMetadata(); md != nil { - if ct := md.GetCreationTimestamp(); ct != nil { - return ct.AsTime(), nil - } - } - return time.Time{}, fmt.Errorf("%w: event %s has no creation_timestamp", ErrDataQuality, m.ci.GetId()) - - case privatev1.EventType_EVENT_TYPE_OBJECT_DELETED: - if md := m.ci.GetMetadata(); md != nil { - if dt := md.GetDeletionTimestamp(); dt != nil { - return dt.AsTime(), nil - } - } - return time.Time{}, fmt.Errorf("%w: event %s has no deletion_timestamp", ErrDataQuality, m.ci.GetId()) - - default: - if s := m.ci.GetStatus(); s != nil { - if t := s.GetStateTransitionTime(); t != nil { - return t.AsTime(), nil - } - } - return time.Time{}, fmt.Errorf("%w: event %s has no state_transition_time", ErrDataQuality, m.ci.GetId()) - } + return ResolveTransitionTime(eventType, + m.ci.GetMetadata().GetCreationTimestamp(), + m.ci.GetMetadata().GetDeletionTimestamp(), + m.ci.GetStatus().GetStateTransitionTime(), + m.ci.GetId()) } diff --git a/osac-metering/metering-service/internal/events/mapper.go b/osac-metering/metering-service/internal/events/mapper.go index a03bb6942..cdf5fd1e3 100644 --- a/osac-metering/metering-service/internal/events/mapper.go +++ b/osac-metering/metering-service/internal/events/mapper.go @@ -10,10 +10,7 @@ import ( privatev1 "github.com/osac-project/osac-metering/internal/api/osac/private/v1" ) -var ( - ErrDataQuality = errors.New("data quality") - ErrTransientState = errors.New("transient state: update projection only, no CloudEvent") -) +var ErrDataQuality = errors.New("data quality") // ResourceMapper extracts metering data from a resource-specific Event payload. // Each OSAC resource type (ComputeInstance, ClusterOrder, etc.) implements this. @@ -39,12 +36,13 @@ type StateContext struct { WasBillable bool BillableSince *time.Time DurationSeconds *float64 - NewDimensions map[string]any } // MapWatchEvent converts a fulfillment-service Watch Event into a CloudEvents 1.0 -// event using the appropriate ResourceMapper for the payload type. -func MapWatchEvent(event *privatev1.Event, mapper ResourceMapper, stateCtx *StateContext) (*cloudevents.Event, error) { +// event. billingDims is the billing dimensions to embed in the event payload — +// callers pass per-component flat dims (from decomposition) or top-level-only +// dims (for audit events), never the nested stored form directly. +func MapWatchEvent(event *privatev1.Event, mapper ResourceMapper, stateCtx *StateContext, billingDims map[string]any) (*cloudevents.Event, error) { previousState := stateCtx.PreviousState ceType, err := mapper.CloudEventType(event.GetType(), previousState) @@ -77,26 +75,7 @@ func MapWatchEvent(event *privatev1.Event, mapper ResourceMapper, stateCtx *Stat } SetOSACExtensions(&ce, mapper.ResourceID(), mapper.ResourceType(), mapper.TenantID(), projectID) - var prevStatePtr *string - if stateCtx.PreviousState != "" { - prevStatePtr = &stateCtx.PreviousState - } - durationPtr := stateCtx.DurationSeconds - - data := meteringData{ - ResourceID: mapper.ResourceID(), - ResourceType: mapper.ResourceType(), - TenantID: mapper.TenantID(), - ProjectID: mapper.ProjectID(), - CatalogItemID: mapper.CatalogItemID(), - TemplateID: mapper.TemplateID(), - PreviousState: prevStatePtr, - CurrentState: mapper.CurrentState(), - TransitionTime: transitionTime.Format(time.RFC3339Nano), - DurationSeconds: durationPtr, - BillingDimensions: mapper.BillingDimensionsMap(), - SchemaVersion: "v1", - } + data := BuildLifecycleData(mapper, billingDims, stateCtx.PreviousState, stateCtx.DurationSeconds, transitionTime) if err := ce.SetData(cloudevents.ApplicationJSON, data); err != nil { return nil, fmt.Errorf("setting CloudEvent data: %w", err) } @@ -116,11 +95,17 @@ func mapperForEvent(event *privatev1.Event) (ResourceMapper, error) { if ci := event.GetComputeInstance(); ci != nil { return &computeInstanceMapper{ci: ci}, nil } + if cl := event.GetCluster(); cl != nil { + return &clusterMapper{cl: cl}, nil + } return nil, fmt.Errorf("unsupported event payload type for event %s", event.GetId()) } -// meteringData is the shared JSON payload for all resource types. -type meteringData struct { +// LifecycleData is the shared JSON payload for lifecycle and scaling events +// across all resource types. Exported and built exclusively through +// BuildLifecycleData so every producer (MapWatchEvent, and the Watch +// Consumer's scaling-event builder) emits the identical shape. +type LifecycleData struct { ResourceID string `json:"resource_id"` ResourceType string `json:"resource_type"` TenantID string `json:"tenant_id"` @@ -135,6 +120,29 @@ type meteringData struct { SchemaVersion string `json:"schema_version"` } +// BuildLifecycleData constructs the shared lifecycle/scaling event payload +// from a resource mapper. +func BuildLifecycleData(mapper ResourceMapper, billingDims map[string]any, previousState string, durationSeconds *float64, transitionTime time.Time) LifecycleData { + var prevStatePtr *string + if previousState != "" { + prevStatePtr = &previousState + } + return LifecycleData{ + ResourceID: mapper.ResourceID(), + ResourceType: mapper.ResourceType(), + TenantID: mapper.TenantID(), + ProjectID: mapper.ProjectID(), + CatalogItemID: mapper.CatalogItemID(), + TemplateID: mapper.TemplateID(), + PreviousState: prevStatePtr, + CurrentState: mapper.CurrentState(), + TransitionTime: transitionTime.Format(time.RFC3339Nano), + DurationSeconds: durationSeconds, + BillingDimensions: billingDims, + SchemaVersion: "v1", + } +} + func NilIfEmpty(s string) *string { if s == "" { return nil diff --git a/osac-metering/metering-service/internal/events/mapper_test.go b/osac-metering/metering-service/internal/events/mapper_test.go index cd569a03d..cd8f15698 100644 --- a/osac-metering/metering-service/internal/events/mapper_test.go +++ b/osac-metering/metering-service/internal/events/mapper_test.go @@ -19,7 +19,7 @@ func mapEvent(event *privatev1.Event, stateCtx *events.StateContext) (*cloudeven if err != nil { return nil, err } - return events.MapWatchEvent(event, mapper, stateCtx) + return events.MapWatchEvent(event, mapper, stateCtx, mapper.BillingDimensionsMap()) } var _ = Describe("MapWatchEvent", func() { @@ -55,208 +55,205 @@ var _ = Describe("MapWatchEvent", func() { } }) - Context("event type mapping", func() { - It("maps OBJECT_CREATED to osac.resource.created.v1", func() { - event := &privatev1.Event{ - Id: "evt-1", - Type: privatev1.EventType_EVENT_TYPE_OBJECT_CREATED, - Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ci}, - } - - ce, err := mapEvent(event, &events.StateContext{}) - Expect(err).NotTo(HaveOccurred()) - Expect(ce.Type()).To(Equal("osac.resource.created.v1")) - }) - - It("maps OBJECT_UPDATED with RUNNING state to osac.resource.started.v1", func() { - ci.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_RUNNING - - event := &privatev1.Event{ - Id: "evt-2", - Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, - Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ci}, - } - - ce, err := mapEvent(event, &events.StateContext{}) - Expect(err).NotTo(HaveOccurred()) - Expect(ce.Type()).To(Equal("osac.resource.started.v1")) - }) - - It("maps OBJECT_UPDATED with STOPPED state to osac.resource.suspended.v1", func() { - ci.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPED - - event := &privatev1.Event{ - Id: "evt-2", - Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, - Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ci}, - } - - ce, err := mapEvent(event, &events.StateContext{}) - Expect(err).NotTo(HaveOccurred()) - Expect(ce.Type()).To(Equal("osac.resource.suspended.v1")) - }) - - It("maps OBJECT_UPDATED with PAUSED state to osac.resource.suspended.v1", func() { - ci.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_PAUSED - - event := &privatev1.Event{ - Id: "evt-2", - Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, - Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ci}, - } - - ce, err := mapEvent(event, &events.StateContext{}) - Expect(err).NotTo(HaveOccurred()) - Expect(ce.Type()).To(Equal("osac.resource.suspended.v1")) - }) - - It("maps OBJECT_UPDATED with FAILED state to osac.resource.suspended.v1", func() { - ci.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_FAILED - - event := &privatev1.Event{ - Id: "evt-2", - Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, - Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ci}, - } - - ce, err := mapEvent(event, &events.StateContext{}) - Expect(err).NotTo(HaveOccurred()) - Expect(ce.Type()).To(Equal("osac.resource.suspended.v1")) - }) - - It("returns ErrTransientState for STOPPING state", func() { - ci.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPING - - event := &privatev1.Event{ - Id: "evt-2", - Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, - Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ci}, - } - - _, err := mapEvent(event, &events.StateContext{}) - Expect(err).To(HaveOccurred()) - Expect(errors.Is(err, events.ErrTransientState)).To(BeTrue()) - }) - - It("returns ErrTransientState for STARTING state", func() { - ci.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STARTING + Context("VMaaS state machine -- full transition matrix", func() { + DescribeTable("resolves correct CloudEvent type for state transitions", + func(currentState privatev1.ComputeInstanceState, previousState string, expectedType string, expectSkip, expectTransient bool) { + ci.Status.State = currentState + event := &privatev1.Event{ + Id: "evt-1", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ci}, + } + stateCtx := &events.StateContext{PreviousState: previousState} + ce, err := mapEvent(event, stateCtx) + if expectSkip { + Expect(err).To(HaveOccurred()) + Expect(errors.Is(err, events.ErrSkipTransition)).To(BeTrue()) + } else if expectTransient { + Expect(err).To(HaveOccurred()) + Expect(errors.Is(err, events.ErrTransientState)).To(BeTrue()) + } else { + Expect(err).NotTo(HaveOccurred()) + Expect(ce.Type()).To(Equal(expectedType)) + } + }, - event := &privatev1.Event{ - Id: "evt-starting", - Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + // --- From "" (initial observation) --- + Entry("initial -> RUNNING -> started.v1", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_RUNNING, "", events.EventStarted, false, false), + Entry("initial -> STOPPED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPED, "", "", true, false), + Entry("initial -> PAUSED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_PAUSED, "", "", true, false), + Entry("initial -> FAILED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_FAILED, "", "", true, false), + Entry("initial -> STOPPING -> transient", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPING, "", "", false, true), + Entry("initial -> STARTING -> transient", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STARTING, "", "", false, true), + Entry("initial -> DELETING -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_DELETING, "", "", true, false), + Entry("initial -> UNSPECIFIED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_UNSPECIFIED, "", "", true, false), + + // --- From RUNNING --- + Entry("RUNNING -> RUNNING -> skip (same-state)", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_RUNNING, "RUNNING", "", true, false), + Entry("RUNNING -> STOPPED -> suspended.v1", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPED, "RUNNING", events.EventSuspended, false, false), + Entry("RUNNING -> PAUSED -> suspended.v1", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_PAUSED, "RUNNING", events.EventSuspended, false, false), + Entry("RUNNING -> FAILED -> suspended.v1", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_FAILED, "RUNNING", events.EventSuspended, false, false), + Entry("RUNNING -> STOPPING -> transient", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPING, "RUNNING", "", false, true), + Entry("RUNNING -> STARTING -> transient", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STARTING, "RUNNING", "", false, true), + Entry("RUNNING -> DELETING -> suspended.v1", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_DELETING, "RUNNING", events.EventSuspended, false, false), + Entry("RUNNING -> UNSPECIFIED -> suspended.v1", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_UNSPECIFIED, "RUNNING", events.EventSuspended, false, false), + + // --- From STOPPED --- + Entry("STOPPED -> RUNNING -> resumed.v1", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_RUNNING, "STOPPED", events.EventResumed, false, false), + Entry("STOPPED -> STOPPED -> skip (same-state)", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPED, "STOPPED", "", true, false), + Entry("STOPPED -> PAUSED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_PAUSED, "STOPPED", "", true, false), + Entry("STOPPED -> FAILED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_FAILED, "STOPPED", "", true, false), + Entry("STOPPED -> STOPPING -> transient", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPING, "STOPPED", "", false, true), + Entry("STOPPED -> STARTING -> transient", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STARTING, "STOPPED", "", false, true), + Entry("STOPPED -> DELETING -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_DELETING, "STOPPED", "", true, false), + Entry("STOPPED -> UNSPECIFIED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_UNSPECIFIED, "STOPPED", "", true, false), + + // --- From PAUSED --- + Entry("PAUSED -> RUNNING -> resumed.v1", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_RUNNING, "PAUSED", events.EventResumed, false, false), + Entry("PAUSED -> STOPPED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPED, "PAUSED", "", true, false), + Entry("PAUSED -> PAUSED -> skip (same-state)", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_PAUSED, "PAUSED", "", true, false), + Entry("PAUSED -> FAILED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_FAILED, "PAUSED", "", true, false), + Entry("PAUSED -> STOPPING -> transient", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPING, "PAUSED", "", false, true), + Entry("PAUSED -> STARTING -> transient", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STARTING, "PAUSED", "", false, true), + Entry("PAUSED -> DELETING -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_DELETING, "PAUSED", "", true, false), + Entry("PAUSED -> UNSPECIFIED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_UNSPECIFIED, "PAUSED", "", true, false), + + // --- From FAILED --- + Entry("FAILED -> RUNNING -> resumed.v1", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_RUNNING, "FAILED", events.EventResumed, false, false), + Entry("FAILED -> STOPPED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPED, "FAILED", "", true, false), + Entry("FAILED -> PAUSED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_PAUSED, "FAILED", "", true, false), + Entry("FAILED -> FAILED -> skip (same-state)", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_FAILED, "FAILED", "", true, false), + Entry("FAILED -> STOPPING -> transient", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPING, "FAILED", "", false, true), + Entry("FAILED -> STARTING -> transient", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STARTING, "FAILED", "", false, true), + Entry("FAILED -> DELETING -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_DELETING, "FAILED", "", true, false), + Entry("FAILED -> UNSPECIFIED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_UNSPECIFIED, "FAILED", "", true, false), + + // --- From STOPPING --- + Entry("STOPPING -> RUNNING -> resumed.v1", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_RUNNING, "STOPPING", events.EventResumed, false, false), + Entry("STOPPING -> STOPPED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPED, "STOPPING", "", true, false), + Entry("STOPPING -> PAUSED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_PAUSED, "STOPPING", "", true, false), + Entry("STOPPING -> FAILED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_FAILED, "STOPPING", "", true, false), + Entry("STOPPING -> STOPPING -> skip (same-state)", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPING, "STOPPING", "", true, false), + Entry("STOPPING -> STARTING -> transient", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STARTING, "STOPPING", "", false, true), + Entry("STOPPING -> DELETING -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_DELETING, "STOPPING", "", true, false), + Entry("STOPPING -> UNSPECIFIED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_UNSPECIFIED, "STOPPING", "", true, false), + + // --- From STARTING --- + Entry("STARTING -> RUNNING -> resumed.v1", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_RUNNING, "STARTING", events.EventResumed, false, false), + Entry("STARTING -> STOPPED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPED, "STARTING", "", true, false), + Entry("STARTING -> PAUSED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_PAUSED, "STARTING", "", true, false), + Entry("STARTING -> FAILED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_FAILED, "STARTING", "", true, false), + Entry("STARTING -> STOPPING -> transient", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPING, "STARTING", "", false, true), + Entry("STARTING -> STARTING -> skip (same-state)", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STARTING, "STARTING", "", true, false), + Entry("STARTING -> DELETING -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_DELETING, "STARTING", "", true, false), + Entry("STARTING -> UNSPECIFIED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_UNSPECIFIED, "STARTING", "", true, false), + + // --- From DELETING --- + Entry("DELETING -> RUNNING -> resumed.v1", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_RUNNING, "DELETING", events.EventResumed, false, false), + Entry("DELETING -> STOPPED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPED, "DELETING", "", true, false), + Entry("DELETING -> PAUSED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_PAUSED, "DELETING", "", true, false), + Entry("DELETING -> FAILED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_FAILED, "DELETING", "", true, false), + Entry("DELETING -> STOPPING -> transient", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPING, "DELETING", "", false, true), + Entry("DELETING -> STARTING -> transient", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STARTING, "DELETING", "", false, true), + Entry("DELETING -> DELETING -> skip (same-state)", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_DELETING, "DELETING", "", true, false), + Entry("DELETING -> UNSPECIFIED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_UNSPECIFIED, "DELETING", "", true, false), + + // --- From UNSPECIFIED --- + Entry("UNSPECIFIED -> RUNNING -> resumed.v1", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_RUNNING, "UNSPECIFIED", events.EventResumed, false, false), + Entry("UNSPECIFIED -> STOPPED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPED, "UNSPECIFIED", "", true, false), + Entry("UNSPECIFIED -> PAUSED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_PAUSED, "UNSPECIFIED", "", true, false), + Entry("UNSPECIFIED -> FAILED -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_FAILED, "UNSPECIFIED", "", true, false), + Entry("UNSPECIFIED -> STOPPING -> transient", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPING, "UNSPECIFIED", "", false, true), + Entry("UNSPECIFIED -> STARTING -> transient", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STARTING, "UNSPECIFIED", "", false, true), + Entry("UNSPECIFIED -> DELETING -> skip", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_DELETING, "UNSPECIFIED", "", true, false), + Entry("UNSPECIFIED -> UNSPECIFIED -> skip (same-state)", + privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_UNSPECIFIED, "UNSPECIFIED", "", true, false), + ) + + It("returns ErrUnsupportedEvent for OBJECT_SIGNALED", func() { + event := &privatev1.Event{ + Id: "evt-signaled", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_SIGNALED, Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ci}, } _, err := mapEvent(event, &events.StateContext{}) Expect(err).To(HaveOccurred()) - Expect(errors.Is(err, events.ErrTransientState)).To(BeTrue()) - }) - - It("maps DELETING to osac.resource.suspended.v1", func() { - ci.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_DELETING - - event := &privatev1.Event{ - Id: "evt-deleting", - Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, - Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ci}, - } - - stateCtx := &events.StateContext{PreviousState: "RUNNING"} - ce, err := mapEvent(event, stateCtx) - Expect(err).NotTo(HaveOccurred()) - Expect(ce.Type()).To(Equal("osac.resource.suspended.v1")) - }) - - It("maps STOPPED→RUNNING to osac.resource.resumed.v1 with state context", func() { - ci.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_RUNNING - - event := &privatev1.Event{ - Id: "evt-resumed-1", - Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, - Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ci}, - } - - stateCtx := &events.StateContext{PreviousState: "STOPPED"} - ce, err := mapEvent(event, stateCtx) - Expect(err).NotTo(HaveOccurred()) - Expect(ce.Type()).To(Equal("osac.resource.resumed.v1")) - }) - - It("maps PAUSED→RUNNING to osac.resource.resumed.v1 with state context", func() { - ci.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_RUNNING - - event := &privatev1.Event{ - Id: "evt-resumed-2", - Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, - Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ci}, - } - - stateCtx := &events.StateContext{PreviousState: "PAUSED"} - ce, err := mapEvent(event, stateCtx) - Expect(err).NotTo(HaveOccurred()) - Expect(ce.Type()).To(Equal("osac.resource.resumed.v1")) - }) - - It("maps STARTING→RUNNING to osac.resource.started.v1 with state context", func() { - ci.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_RUNNING - - event := &privatev1.Event{ - Id: "evt-started", - Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, - Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ci}, - } - - stateCtx := &events.StateContext{PreviousState: "STARTING"} - ce, err := mapEvent(event, stateCtx) - Expect(err).NotTo(HaveOccurred()) - Expect(ce.Type()).To(Equal("osac.resource.started.v1")) - }) - - It("maps FAILED→RUNNING to osac.resource.started.v1", func() { - ci.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_RUNNING - - event := &privatev1.Event{ - Id: "evt-failed-to-running", - Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, - Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ci}, - } - - stateCtx := &events.StateContext{PreviousState: "FAILED"} - ce, err := mapEvent(event, stateCtx) - Expect(err).NotTo(HaveOccurred()) - Expect(ce.Type()).To(Equal("osac.resource.started.v1")) + Expect(errors.Is(err, events.ErrUnsupportedEvent)).To(BeTrue()) }) - It("maps RUNNING→RUNNING (prev=RUNNING) to osac.resource.started.v1", func() { - ci.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_RUNNING - - event := &privatev1.Event{ - Id: "evt-running-to-running", - Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, - Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ci}, - } - - stateCtx := &events.StateContext{PreviousState: "RUNNING"} - ce, err := mapEvent(event, stateCtx) - Expect(err).NotTo(HaveOccurred()) - Expect(ce.Type()).To(Equal("osac.resource.started.v1")) - }) - - It("maps UNSPECIFIED to osac.resource.updated.v1", func() { - ci.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_UNSPECIFIED - - event := &privatev1.Event{ - Id: "evt-unspecified", - Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, - Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ci}, - } - - ce, err := mapEvent(event, &events.StateContext{}) - Expect(err).NotTo(HaveOccurred()) - Expect(ce.Type()).To(Equal("osac.resource.updated.v1")) - }) - - It("returns error for unknown state (default branch)", func() { + It("returns error for unknown state (missing table entry)", func() { ci.Status.State = privatev1.ComputeInstanceState(9999) event := &privatev1.Event{ @@ -267,11 +264,11 @@ var _ = Describe("MapWatchEvent", func() { _, err := mapEvent(event, &events.StateContext{}) Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("unexpected compute instance state transition")) + Expect(err.Error()).To(ContainSubstring("unexpected state transition")) Expect(errors.Is(err, events.ErrTransientState)).To(BeFalse()) }) - It("maps RUNNING→STOPPED with duration to osac.resource.suspended.v1", func() { + It("maps RUNNING->STOPPED with duration to osac.resource.suspended.v1", func() { ci.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPED event := &privatev1.Event{ @@ -287,14 +284,14 @@ var _ = Describe("MapWatchEvent", func() { } ce, err := mapEvent(event, stateCtx) Expect(err).NotTo(HaveOccurred()) - Expect(ce.Type()).To(Equal("osac.resource.suspended.v1")) + Expect(ce.Type()).To(Equal(events.EventSuspended)) var data map[string]any Expect(json.Unmarshal(ce.Data(), &data)).To(Succeed()) Expect(data["duration_seconds"]).To(BeNumerically("==", 7200.0)) }) - It("maps RUNNING→FAILED (prev=RUNNING) to osac.resource.suspended.v1", func() { + It("maps RUNNING->FAILED (prev=RUNNING) to osac.resource.suspended.v1", func() { ci.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_FAILED event := &privatev1.Event{ @@ -306,7 +303,7 @@ var _ = Describe("MapWatchEvent", func() { stateCtx := &events.StateContext{PreviousState: "RUNNING"} ce, err := mapEvent(event, stateCtx) Expect(err).NotTo(HaveOccurred()) - Expect(ce.Type()).To(Equal("osac.resource.suspended.v1")) + Expect(ce.Type()).To(Equal(events.EventSuspended)) }) It("includes previous_state and duration_seconds when state context provided", func() { @@ -332,6 +329,18 @@ var _ = Describe("MapWatchEvent", func() { Expect(data["duration_seconds"]).To(BeNumerically("==", 3600.5)) }) + It("maps OBJECT_CREATED to osac.resource.created.v1", func() { + event := &privatev1.Event{ + Id: "evt-1", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_CREATED, + Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ci}, + } + + ce, err := mapEvent(event, &events.StateContext{}) + Expect(err).NotTo(HaveOccurred()) + Expect(ce.Type()).To(Equal(events.EventCreated)) + }) + It("maps OBJECT_DELETED to osac.resource.deleted.v1", func() { ci.Metadata.DeletionTimestamp = timestamppb.Now() @@ -343,7 +352,7 @@ var _ = Describe("MapWatchEvent", func() { ce, err := mapEvent(event, &events.StateContext{}) Expect(err).NotTo(HaveOccurred()) - Expect(ce.Type()).To(Equal("osac.resource.deleted.v1")) + Expect(ce.Type()).To(Equal(events.EventDeleted)) }) }) @@ -545,11 +554,11 @@ var _ = Describe("MapWatchEvent", func() { Expect(err.Error()).To(ContainSubstring("unsupported")) }) - It("returns an error for non-ComputeInstance payload", func() { + It("returns an error for unsupported payload type", func() { event := &privatev1.Event{ Id: "evt-1", Type: privatev1.EventType_EVENT_TYPE_OBJECT_CREATED, - Payload: &privatev1.Event_Cluster{Cluster: &privatev1.Cluster{}}, + Payload: &privatev1.Event_Hub{Hub: &privatev1.Hub{}}, } _, err := mapEvent(event, &events.StateContext{}) @@ -768,7 +777,7 @@ var _ = Describe("MapWatchEvent", func() { _, err := mapEvent(event, &events.StateContext{}) Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(ContainSubstring("no creation_timestamp"))) + Expect(err).To(MatchError(ContainSubstring("no timestamp for event type"))) Expect(errors.Is(err, events.ErrDataQuality)).To(BeTrue()) }) @@ -798,7 +807,7 @@ var _ = Describe("MapWatchEvent", func() { _, err := mapEvent(event, &events.StateContext{}) Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(ContainSubstring("no state_transition_time"))) + Expect(err).To(MatchError(ContainSubstring("no timestamp for event type"))) Expect(errors.Is(err, events.ErrDataQuality)).To(BeTrue()) }) @@ -828,7 +837,7 @@ var _ = Describe("MapWatchEvent", func() { _, err := mapEvent(event, &events.StateContext{}) Expect(err).To(HaveOccurred()) - Expect(err).To(MatchError(ContainSubstring("no deletion_timestamp"))) + Expect(err).To(MatchError(ContainSubstring("no timestamp for event type"))) Expect(errors.Is(err, events.ErrDataQuality)).To(BeTrue()) }) }) @@ -873,3 +882,49 @@ var _ = Describe("DimensionsEqual", func() { Expect(events.DimensionsEqual(nil, nil)).To(BeTrue()) }) }) + +var _ = Describe("VMaaS transition table completeness", func() { + stateProtoMap := map[string]privatev1.ComputeInstanceState{ + "RUNNING": privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_RUNNING, + "STOPPED": privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPED, + "PAUSED": privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_PAUSED, + "FAILED": privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_FAILED, + "STOPPING": privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPING, + "STARTING": privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STARTING, + "DELETING": privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_DELETING, + "UNSPECIFIED": privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_UNSPECIFIED, + } + + It("covers every (from, to) state pair from all proto states plus empty initial", func() { + fromStates := []string{"", "RUNNING", "STOPPED", "PAUSED", "FAILED", "STOPPING", "STARTING", "DELETING", "UNSPECIFIED"} + toStates := []string{"RUNNING", "STOPPED", "PAUSED", "FAILED", "STOPPING", "STARTING", "DELETING", "UNSPECIFIED"} + + for _, from := range fromStates { + for _, to := range toStates { + ci := &privatev1.ComputeInstance{ + Id: "ci-completeness", + Metadata: &privatev1.Metadata{Tenant: "t", CreationTimestamp: timestamppb.Now()}, + Spec: &privatev1.ComputeInstanceSpec{}, + Status: &privatev1.ComputeInstanceStatus{ + State: stateProtoMap[to], + StateTransitionTime: timestamppb.Now(), + }, + } + + event := &privatev1.Event{ + Id: "evt-completeness", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ci}, + } + + stateCtx := &events.StateContext{PreviousState: from} + _, err := mapEvent(event, stateCtx) + + Expect(err == nil || + errors.Is(err, events.ErrSkipTransition) || + errors.Is(err, events.ErrTransientState)).To(BeTrue(), + "transition %s -> %s returned unexpected error: %v", from, to, err) + } + } + }) +}) diff --git a/osac-metering/metering-service/internal/events/transitions.go b/osac-metering/metering-service/internal/events/transitions.go new file mode 100644 index 000000000..bab997ac5 --- /dev/null +++ b/osac-metering/metering-service/internal/events/transitions.go @@ -0,0 +1,151 @@ +/* +Copyright (c) 2026 Red Hat, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +in compliance with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 +*/ + +package events + +import ( + "errors" + "fmt" + "time" + + cloudevents "github.com/cloudevents/sdk-go/v2" + "google.golang.org/protobuf/types/known/timestamppb" + + privatev1 "github.com/osac-project/osac-metering/internal/api/osac/private/v1" +) + +var ( + ErrTransientState = errors.New("transient state: update projection only, no CloudEvent") + ErrSkipTransition = errors.New("no billing boundary: skip event, check for scaling") + ErrUnsupportedEvent = errors.New("unsupported event type") +) + +// CloudEvent type constants. +const ( + EventCreated = "osac.resource.created.v1" + EventStarted = "osac.resource.started.v1" + EventResumed = "osac.resource.resumed.v1" + EventSuspended = "osac.resource.suspended.v1" + EventDeleted = "osac.resource.deleted.v1" + EventUpdated = "osac.resource.updated.v1" + EventHeartbeat = "osac.resource.heartbeat.v1" + EventCorrection = "osac.resource.correction.v1" +) + +// Resource type constants. +const ( + ResourceTypeComputeInstance = "compute_instance" + ResourceTypeClusterOrder = "cluster_order" +) + +// StateEmpty is the empty previous state for initial transitions. +const StateEmpty = "" + +// TransitionKey identifies a state transition by (previous, current) state. +type TransitionKey struct { + From string + To string +} + +// TransitionResult defines what happens on a state transition. +type TransitionResult struct { + EventType string // CloudEvent type to emit (empty when Transient or Skip) + Transient bool // projection-only update, no CloudEvent + Skip bool // no projection update, no CloudEvent (non-billing transition) +} + +// TransitionTable maps (previous, current) state pairs to their billing effect. +// Missing entries are invalid transitions — resolveTransition returns an error. +type TransitionTable map[TransitionKey]TransitionResult + +// resolveTransition looks up the event type for a state transition. +// Exact (from, to) match only — missing entry = error (fail fast on unknown transitions). +func resolveTransition(table TransitionTable, from, to string) (string, error) { + if result, ok := table[TransitionKey{from, to}]; ok { + return applyResult(result) + } + return "", fmt.Errorf("unexpected state transition: %s -> %s", from, to) +} + +func applyResult(r TransitionResult) (string, error) { + if r.Skip { + return "", ErrSkipTransition + } + if r.Transient { + return "", ErrTransientState + } + return r.EventType, nil +} + +// fixedEventTypes maps event types that always produce the same CloudEvent type +// regardless of state transition. +var fixedEventTypes = map[privatev1.EventType]string{ + privatev1.EventType_EVENT_TYPE_OBJECT_CREATED: EventCreated, + privatev1.EventType_EVENT_TYPE_OBJECT_DELETED: EventDeleted, +} + +// ResolveCloudEventType returns the CloudEvent type for a given proto event type +// and state transition. CREATED and DELETED are fixed; UPDATED delegates to the +// transition table. +func ResolveCloudEventType(table TransitionTable, eventType privatev1.EventType, previousState, currentState string) (string, error) { + if ceType, ok := fixedEventTypes[eventType]; ok { + return ceType, nil + } + if eventType == privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED { + return resolveTransition(table, previousState, currentState) + } + return "", fmt.Errorf("%w: %v", ErrUnsupportedEvent, eventType) +} + +// ResolveTransitionTime selects the appropriate timestamp for a given event type. +func ResolveTransitionTime(eventType privatev1.EventType, creation, deletion, stateTransition *timestamppb.Timestamp, resourceID string) (time.Time, error) { + timestamps := map[privatev1.EventType]*timestamppb.Timestamp{ + privatev1.EventType_EVENT_TYPE_OBJECT_CREATED: creation, + privatev1.EventType_EVENT_TYPE_OBJECT_DELETED: deletion, + privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED: stateTransition, + } + + ts, ok := timestamps[eventType] + if !ok { + return time.Time{}, fmt.Errorf("%w for timestamp: %v", ErrUnsupportedEvent, eventType) + } + if ts == nil { + return time.Time{}, fmt.Errorf("%w: resource %s has no timestamp for event type %v", ErrDataQuality, resourceID, eventType) + } + return ts.AsTime(), nil +} + +// EventBuilder creates a CloudEvent from billing dimensions and an event ID. +type EventBuilder func(dims map[string]any, eventID string) (cloudevents.Event, error) + +// EventDecomposer produces one or more CloudEvents from billing dimensions. +type EventDecomposer func(dims map[string]any, baseID string, buildFn EventBuilder) ([]cloudevents.Event, error) + +func singleEvent(dims map[string]any, baseID string, buildFn EventBuilder) ([]cloudevents.Event, error) { + ce, err := buildFn(dims, baseID) + if err != nil { + return nil, err + } + return []cloudevents.Event{ce}, nil +} + +var resourceDecomposers = map[string]EventDecomposer{ + ResourceTypeComputeInstance: singleEvent, + ResourceTypeClusterOrder: DecomposeClusterEvents, +} + +// BuildResourceEvents dispatches event building to the correct decomposer +// for the given resource type. +func BuildResourceEvents(resourceType string, dims map[string]any, baseID string, buildFn EventBuilder) ([]cloudevents.Event, error) { + decomposer, ok := resourceDecomposers[resourceType] + if !ok { + return nil, fmt.Errorf("unknown resource type for event decomposition: %s", resourceType) + } + return decomposer(dims, baseID, buildFn) +} diff --git a/osac-metering/metering-service/internal/events/transitions_test.go b/osac-metering/metering-service/internal/events/transitions_test.go new file mode 100644 index 000000000..61396299d --- /dev/null +++ b/osac-metering/metering-service/internal/events/transitions_test.go @@ -0,0 +1,266 @@ +/* +Copyright (c) 2026 Red Hat, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +in compliance with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 +*/ + +package events_test + +import ( + "errors" + "fmt" + "time" + + cloudevents "github.com/cloudevents/sdk-go/v2" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "google.golang.org/protobuf/types/known/timestamppb" + + privatev1 "github.com/osac-project/osac-metering/internal/api/osac/private/v1" + "github.com/osac-project/osac-metering/internal/events" +) + +var _ = Describe("ResolveCloudEventType", func() { + It("returns created.v1 for CREATED event type", func() { + ceType, err := events.ResolveCloudEventType( + events.TransitionTable{}, + privatev1.EventType_EVENT_TYPE_OBJECT_CREATED, + "", "", + ) + Expect(err).NotTo(HaveOccurred()) + Expect(ceType).To(Equal(events.EventCreated)) + }) + + It("returns deleted.v1 for DELETED event type", func() { + ceType, err := events.ResolveCloudEventType( + events.TransitionTable{}, + privatev1.EventType_EVENT_TYPE_OBJECT_DELETED, + "", "", + ) + Expect(err).NotTo(HaveOccurred()) + Expect(ceType).To(Equal(events.EventDeleted)) + }) + + It("delegates to transition table for UPDATED event type", func() { + table := events.TransitionTable{ + {From: "A", To: "B"}: {EventType: events.EventStarted}, + } + ceType, err := events.ResolveCloudEventType( + table, + privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + "A", "B", + ) + Expect(err).NotTo(HaveOccurred()) + Expect(ceType).To(Equal(events.EventStarted)) + }) + + It("returns error for UPDATED with missing table entry", func() { + table := events.TransitionTable{ + {From: "A", To: "B"}: {EventType: events.EventStarted}, + } + _, err := events.ResolveCloudEventType( + table, + privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + "X", "Y", + ) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unexpected state transition")) + }) + + It("returns error for unknown event type", func() { + _, err := events.ResolveCloudEventType( + events.TransitionTable{}, + privatev1.EventType(9999), + "", "", + ) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unsupported event type")) + }) +}) + +var _ = Describe("ResolveTransitionTime", func() { + var ( + creationTS *timestamppb.Timestamp + deletionTS *timestamppb.Timestamp + stateTransitionTS *timestamppb.Timestamp + ) + + BeforeEach(func() { + creationTS = timestamppb.New(time.Date(2026, 7, 1, 10, 0, 0, 0, time.UTC)) + deletionTS = timestamppb.New(time.Date(2026, 7, 2, 12, 0, 0, 0, time.UTC)) + stateTransitionTS = timestamppb.New(time.Date(2026, 7, 1, 11, 30, 0, 0, time.UTC)) + }) + + It("returns creation timestamp for CREATED event type", func() { + t, err := events.ResolveTransitionTime( + privatev1.EventType_EVENT_TYPE_OBJECT_CREATED, + creationTS, deletionTS, stateTransitionTS, "res-1", + ) + Expect(err).NotTo(HaveOccurred()) + Expect(t).To(Equal(creationTS.AsTime())) + }) + + It("returns deletion timestamp for DELETED event type", func() { + t, err := events.ResolveTransitionTime( + privatev1.EventType_EVENT_TYPE_OBJECT_DELETED, + creationTS, deletionTS, stateTransitionTS, "res-1", + ) + Expect(err).NotTo(HaveOccurred()) + Expect(t).To(Equal(deletionTS.AsTime())) + }) + + It("returns state transition timestamp for UPDATED event type", func() { + t, err := events.ResolveTransitionTime( + privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + creationTS, deletionTS, stateTransitionTS, "res-1", + ) + Expect(err).NotTo(HaveOccurred()) + Expect(t).To(Equal(stateTransitionTS.AsTime())) + }) + + It("returns ErrUnsupportedEvent for OBJECT_SIGNALED", func() { + _, err := events.ResolveTransitionTime( + privatev1.EventType_EVENT_TYPE_OBJECT_SIGNALED, + creationTS, deletionTS, stateTransitionTS, "res-1", + ) + Expect(err).To(HaveOccurred()) + Expect(errors.Is(err, events.ErrUnsupportedEvent)).To(BeTrue()) + }) + + It("returns ErrUnsupportedEvent for unknown event type", func() { + _, err := events.ResolveTransitionTime( + privatev1.EventType(9999), + creationTS, deletionTS, stateTransitionTS, "res-1", + ) + Expect(err).To(HaveOccurred()) + Expect(errors.Is(err, events.ErrUnsupportedEvent)).To(BeTrue()) + }) + + It("returns ErrDataQuality when creation timestamp is nil for CREATED", func() { + _, err := events.ResolveTransitionTime( + privatev1.EventType_EVENT_TYPE_OBJECT_CREATED, + nil, deletionTS, stateTransitionTS, "res-1", + ) + Expect(err).To(HaveOccurred()) + Expect(errors.Is(err, events.ErrDataQuality)).To(BeTrue()) + }) + + It("returns ErrDataQuality when deletion timestamp is nil for DELETED", func() { + _, err := events.ResolveTransitionTime( + privatev1.EventType_EVENT_TYPE_OBJECT_DELETED, + creationTS, nil, stateTransitionTS, "res-1", + ) + Expect(err).To(HaveOccurred()) + Expect(errors.Is(err, events.ErrDataQuality)).To(BeTrue()) + }) + + It("returns ErrDataQuality when state transition timestamp is nil for UPDATED", func() { + _, err := events.ResolveTransitionTime( + privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + creationTS, deletionTS, nil, "res-1", + ) + Expect(err).To(HaveOccurred()) + Expect(errors.Is(err, events.ErrDataQuality)).To(BeTrue()) + }) +}) + +var _ = Describe("BuildResourceEvents", func() { + simpleBuildFn := func(dims map[string]any, eventID string) (cloudevents.Event, error) { + ce := cloudevents.NewEvent() + ce.SetID(eventID) + if err := ce.SetData(cloudevents.ApplicationJSON, dims); err != nil { + return cloudevents.Event{}, fmt.Errorf("setting data: %w", err) + } + return ce, nil + } + + It("returns a single event for compute_instance", func() { + dims := map[string]any{"instance_type": "large-gpu"} + result, err := events.BuildResourceEvents( + events.ResourceTypeComputeInstance, dims, "evt-ci-1", simpleBuildFn, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(HaveLen(1)) + Expect(result[0].ID()).To(Equal("evt-ci-1")) + }) + + It("decomposes cluster_order into per-component events", func() { + dims := map[string]any{ + "cluster_template": "ocp-ci-small", + "components": []any{ + map[string]any{ + "node_set": "_control_plane", + "component": "control_plane", + "host_type": "_control_plane", + "node_count": int32(1), + }, + map[string]any{ + "node_set": "gpu-workers", + "component": "worker", + "host_type": "gpu-h100", + "node_count": int32(2), + }, + }, + } + + result, err := events.BuildResourceEvents( + events.ResourceTypeClusterOrder, dims, "evt-cl-1", simpleBuildFn, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(result).To(HaveLen(2)) + }) + + It("returns error for unknown resource type", func() { + _, err := events.BuildResourceEvents( + "unknown_resource", map[string]any{}, "evt-1", simpleBuildFn, + ) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unknown resource type for event decomposition")) + }) +}) + +var _ = Describe("resolveTransition (indirect via ResolveCloudEventType)", func() { + It("returns correct event type for an exact table match", func() { + table := events.TransitionTable{ + {From: "STOPPED", To: "RUNNING"}: {EventType: events.EventResumed}, + } + ceType, err := events.ResolveCloudEventType( + table, + privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + "STOPPED", "RUNNING", + ) + Expect(err).NotTo(HaveOccurred()) + Expect(ceType).To(Equal(events.EventResumed)) + }) + + It("returns error for a missing table entry", func() { + table := events.TransitionTable{ + {From: "STOPPED", To: "RUNNING"}: {EventType: events.EventResumed}, + } + _, err := events.ResolveCloudEventType( + table, + privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + "FAILED", "RUNNING", + ) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unexpected state transition")) + }) + + It("does not fall back to wildcard-like partial matches", func() { + // Table has only {"A", "B"} entry. If wildcards existed, + // {"C", "B"} might match a wildcard on From. Verify it does not. + table := events.TransitionTable{ + {From: "A", To: "B"}: {EventType: events.EventStarted}, + } + _, err := events.ResolveCloudEventType( + table, + privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + "C", "B", + ) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unexpected state transition")) + }) +}) diff --git a/osac-metering/metering-service/internal/heartbeat/generator.go b/osac-metering/metering-service/internal/heartbeat/generator.go index 007e9852a..576fc0f1a 100644 --- a/osac-metering/metering-service/internal/heartbeat/generator.go +++ b/osac-metering/metering-service/internal/heartbeat/generator.go @@ -16,7 +16,6 @@ import ( cloudevents "github.com/cloudevents/sdk-go/v2" "github.com/go-logr/logr" - "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" @@ -92,41 +91,65 @@ func (g *Generator) tick(ctx context.Context) error { now := time.Now().UTC() var publishedIDs []string - // Partial checkpoint: on Kafka failure, already-published IDs are checkpointed - // to prevent duplicate heartbeats on retry. At scale (>10K VMs), consider - // Kafka transactional producer for atomic batch publish. + // One resource's publish failure is isolated to that resource: it is + // skipped for this tick (so its checkpoint does not advance and the + // reconciler's stale-heartbeat detection picks it back up), but it does + // not block heartbeats for the other resources in the same tick. for i := range billable { - ce, ceErr := g.buildHeartbeatEvent(&billable[i], now) + hbEvents, ceErr := g.buildHeartbeatEvents(&billable[i], now) if ceErr != nil { g.logger.Error(ceErr, "building heartbeat event, skipping resource", "resource_id", billable[i].ResourceID) continue } - if err := g.publisher.Publish(ctx, ce); err != nil { - if len(publishedIDs) > 0 { - if cpErr := g.store.UpdateLastHeartbeat(ctx, publishedIDs, now); cpErr != nil { - g.logger.Error(cpErr, "failed to checkpoint partial heartbeat progress", - "published", len(publishedIDs)) - } - } - return fmt.Errorf("publishing heartbeat for %s: %w", billable[i].ResourceID, err) + if !g.publishResourceHeartbeats(ctx, hbEvents, billable[i].ResourceID) { + continue } publishedIDs = append(publishedIDs, billable[i].ResourceID) } - if err := g.store.UpdateLastHeartbeat(ctx, publishedIDs, now); err != nil { - return fmt.Errorf("updating last heartbeat: %w", err) + if len(publishedIDs) > 0 { + if err := g.store.UpdateLastHeartbeat(ctx, publishedIDs, now); err != nil { + return fmt.Errorf("updating last heartbeat: %w", err) + } } - g.logger.Info("heartbeat tick completed", "count", len(publishedIDs)) + g.logger.Info("heartbeat tick completed", "published", len(publishedIDs), "total", len(billable)) return nil } -func (g *Generator) buildHeartbeatEvent(state *projection.ResourceState, now time.Time) (cloudevents.Event, error) { +// publishResourceHeartbeats publishes every event in one resource's N+1 +// fan-out. A failure partway through means the resource is not checkpointed +// this tick, but it does not prevent other resources from heartbeating. +func (g *Generator) publishResourceHeartbeats(ctx context.Context, hbEvents []cloudevents.Event, resourceID string) bool { + for j := range hbEvents { + if err := g.publisher.Publish(ctx, hbEvents[j]); err != nil { + g.logger.Error(err, "publishing heartbeat, resource will not be checkpointed this tick", + "resource_id", resourceID) + return false + } + } + return true +} + +func (g *Generator) buildHeartbeatEvents(state *projection.ResourceState, now time.Time) ([]cloudevents.Event, error) { + buildFn := func(dims map[string]any, eventID string) (cloudevents.Event, error) { + return g.buildHeartbeatEvent(state, eventID, dims, now) + } + + // Base ID should be reproducible for a given resource and heartbeat + // window, so that building this same tick's events more than once — + // e.g. a future in-tick retry — reproduces the same per-component + // CloudEvent IDs. + baseID := fmt.Sprintf("hb/%s/%d", state.ResourceID, now.Truncate(g.interval).Unix()) + return events.BuildResourceEvents(state.ResourceType, state.BillingDimensions, baseID, buildFn) +} + +func (g *Generator) buildHeartbeatEvent(state *projection.ResourceState, eventID string, dims map[string]any, now time.Time) (cloudevents.Event, error) { ce := cloudevents.NewEvent() - ce.SetID(uuid.NewString()) + ce.SetID(eventID) ce.SetSource("osac-metering") - ce.SetType("osac.resource.heartbeat.v1") + ce.SetType(events.EventHeartbeat) ce.SetTime(now) events.SetOSACExtensions(&ce, state.ResourceID, state.ResourceType, state.TenantID, state.ProjectID) @@ -143,7 +166,7 @@ func (g *Generator) buildHeartbeatEvent(state *projection.ResourceState, now tim ProjectID: events.NilIfEmpty(state.ProjectID), CurrentState: state.CurrentState, DurationSeconds: durationSeconds, - BillingDimensions: state.BillingDimensions, + BillingDimensions: dims, SchemaVersion: "v1", } if err := ce.SetData(cloudevents.ApplicationJSON, data); err != nil { diff --git a/osac-metering/metering-service/internal/heartbeat/generator_internal_test.go b/osac-metering/metering-service/internal/heartbeat/generator_internal_test.go new file mode 100644 index 000000000..9eb1fea3f --- /dev/null +++ b/osac-metering/metering-service/internal/heartbeat/generator_internal_test.go @@ -0,0 +1,64 @@ +/* +Copyright (c) 2026 Red Hat, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +in compliance with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 +*/ + +package heartbeat + +import ( + "testing" + "time" + + "github.com/osac-project/osac-metering/internal/events" + "github.com/osac-project/osac-metering/internal/projection" +) + +func TestBuildHeartbeatEventsStableIDWithinSameWindow(t *testing.T) { + g := &Generator{interval: 60 * time.Second} + state := &projection.ResourceState{ + ResourceID: "vm-1", + ResourceType: events.ResourceTypeComputeInstance, + BillingDimensions: map[string]any{"instance_type": "m5.large"}, + } + + windowStart := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) + first, err := g.buildHeartbeatEvents(state, windowStart.Add(5*time.Second)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + second, err := g.buildHeartbeatEvents(state, windowStart.Add(45*time.Second)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if first[0].ID() != second[0].ID() { + t.Errorf("two builds within the same %s heartbeat window should share a CloudEvent ID, got %q and %q", g.interval, first[0].ID(), second[0].ID()) + } +} + +func TestBuildHeartbeatEventsNewIDInNextWindow(t *testing.T) { + g := &Generator{interval: 60 * time.Second} + state := &projection.ResourceState{ + ResourceID: "vm-1", + ResourceType: events.ResourceTypeComputeInstance, + BillingDimensions: map[string]any{"instance_type": "m5.large"}, + } + + windowStart := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) + first, err := g.buildHeartbeatEvents(state, windowStart) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + second, err := g.buildHeartbeatEvents(state, windowStart.Add(g.interval)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if first[0].ID() == second[0].ID() { + t.Errorf("builds in different heartbeat windows must not share a CloudEvent ID, both were %q", first[0].ID()) + } +} diff --git a/osac-metering/metering-service/internal/heartbeat/generator_test.go b/osac-metering/metering-service/internal/heartbeat/generator_test.go index e143fb7e7..7246a9f3d 100644 --- a/osac-metering/metering-service/internal/heartbeat/generator_test.go +++ b/osac-metering/metering-service/internal/heartbeat/generator_test.go @@ -12,6 +12,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/osac-project/osac-metering/internal/events" "github.com/osac-project/osac-metering/internal/heartbeat" "github.com/osac-project/osac-metering/internal/projection" ) @@ -53,17 +54,29 @@ func (s *mockStore) UpdateLastHeartbeat(_ context.Context, ids []string, at time } type mockPublisher struct { - mu sync.Mutex - published []cloudevents.Event - err error - failAfter int - callCount int + mu sync.Mutex + published []cloudevents.Event + err error + failAfter int + callCount int + failResourceID string // if set, Publish persistently fails for this resource's events only } func (p *mockPublisher) Publish(_ context.Context, event cloudevents.Event) error { p.mu.Lock() defer p.mu.Unlock() p.callCount++ + if p.failResourceID != "" { + // Self-contained mode: only this resource's events fail, regardless + // of failAfter/err, which drive the (mutually exclusive) call-count-based mode below. + if rid, err := event.Context.GetExtension("osacresourceid"); err == nil { + if s, ok := rid.(string); ok && s == p.failResourceID { + return p.err + } + } + p.published = append(p.published, event) + return nil + } if p.failAfter > 0 && p.callCount > p.failAfter { return p.err } @@ -78,7 +91,7 @@ func makeBillableState(id string) projection.ResourceState { now := time.Now().UTC().Truncate(time.Microsecond) return projection.ResourceState{ ResourceID: id, - ResourceType: "compute_instance", + ResourceType: events.ResourceTypeComputeInstance, TenantID: "tenant-1", ProjectID: "project-1", CurrentState: "RUNNING", @@ -111,7 +124,7 @@ var _ = Describe("Generator", func() { pub.mu.Lock() defer pub.mu.Unlock() Expect(len(pub.published)).To(BeNumerically(">=", 2)) - Expect(pub.published[0].Type()).To(Equal("osac.resource.heartbeat.v1")) + Expect(pub.published[0].Type()).To(Equal(events.EventHeartbeat)) }) It("stops on context cancellation", func() { @@ -193,6 +206,32 @@ var _ = Describe("Generator", func() { Expect(store.updatedIDs).To(HaveLen(2)) }) + It("still heartbeats other resources when one resource's publish persistently fails", func() { + store := &mockStore{ + billable: []projection.ResourceState{ + makeBillableState("vm-fail"), + makeBillableState("vm-ok"), + }, + } + pub := &mockPublisher{ + err: fmt.Errorf("kafka unavailable"), + failResourceID: "vm-fail", + } + gen := heartbeat.NewGenerator(store, pub, logr.Discard(), 100*time.Millisecond) + + ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond) + defer cancel() + + err := gen.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + store.mu.Lock() + defer store.mu.Unlock() + Expect(store.updatedIDs).To(ContainElement("vm-ok"), + "a resource whose publish fails must not block heartbeats for other resources in the same tick") + Expect(store.updatedIDs).NotTo(ContainElement("vm-fail")) + }) + It("fails tick when ListBillable returns error", func() { store := &mockStore{listErr: fmt.Errorf("database unavailable")} pub := &mockPublisher{} @@ -255,4 +294,127 @@ var _ = Describe("Generator", func() { Expect(pub.published[0].Extensions()["osacresourceid"]).To(Equal("vm-1")) }) }) + + Describe("CaaS cluster N+1 heartbeats", func() { + makeClusterBillableState := func(id string) projection.ResourceState { + now := time.Now().UTC().Truncate(time.Microsecond) + return projection.ResourceState{ + ResourceID: id, + ResourceType: events.ResourceTypeClusterOrder, + TenantID: "tenant-1", + ProjectID: "project-1", + CurrentState: "READY", + IsBillable: true, + BillableSince: &now, + BillingDimensions: map[string]any{ + "cluster_template": "ocp-ci-small", + "release_image": "4.17.0", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": float64(1)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": float64(2)}, + }, + }, + } + } + + It("publishes N+1 heartbeats per cluster (1 CP + 1 worker = 2)", func() { + store := &mockStore{ + billable: []projection.ResourceState{makeClusterBillableState("cl-1")}, + } + pub := &mockPublisher{} + gen := heartbeat.NewGenerator(store, pub, logr.Discard(), 100*time.Millisecond) + + ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond) + defer cancel() + + err := gen.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + pub.mu.Lock() + defer pub.mu.Unlock() + Expect(len(pub.published)).To(BeNumerically(">=", 2)) + + components := map[string]bool{} + for _, e := range pub.published { + Expect(e.Type()).To(Equal(events.EventHeartbeat)) + var data map[string]any + Expect(json.Unmarshal(e.Data(), &data)).To(Succeed()) + bd := data["billing_dimensions"].(map[string]any) + Expect(bd).NotTo(HaveKey("components")) + Expect(bd).To(HaveKey("component")) + Expect(bd).To(HaveKey("host_type")) + Expect(bd).To(HaveKey("node_count")) + comp := bd["component"].(string) + ":" + bd["host_type"].(string) + components[comp] = true + } + Expect(components).To(HaveKey("control_plane:_control_plane")) + Expect(components).To(HaveKey("worker:gpu-h100")) + }) + + It("VMaaS and CaaS in same tick produce correct event counts", func() { + store := &mockStore{ + billable: []projection.ResourceState{ + makeBillableState("vm-1"), + makeClusterBillableState("cl-1"), + }, + } + pub := &mockPublisher{} + gen := heartbeat.NewGenerator(store, pub, logr.Discard(), 100*time.Millisecond) + + ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond) + defer cancel() + + err := gen.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + pub.mu.Lock() + defer pub.mu.Unlock() + // Per tick: 1 VMaaS + 2 CaaS (CP + 1 worker) = 3 + Expect(len(pub.published)).To(BeNumerically(">=", 3)) + + store.mu.Lock() + defer store.mu.Unlock() + Expect(store.updatedIDs).To(ContainElement("vm-1")) + Expect(store.updatedIDs).To(ContainElement("cl-1")) + }) + + It("checkpoints cluster ID after all N+1 component heartbeats published", func() { + store := &mockStore{ + billable: []projection.ResourceState{makeClusterBillableState("cl-cp")}, + } + pub := &mockPublisher{} + gen := heartbeat.NewGenerator(store, pub, logr.Discard(), 100*time.Millisecond) + + ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond) + defer cancel() + + err := gen.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + store.mu.Lock() + defer store.mu.Unlock() + Expect(store.updatedIDs).To(ContainElement("cl-cp")) + }) + + It("fails tick on partial N+1 heartbeat publish failure", func() { + store := &mockStore{ + billable: []projection.ResourceState{makeClusterBillableState("cl-fail")}, + } + pub := &mockPublisher{ + err: fmt.Errorf("kafka unavailable"), + failAfter: 1, + } + gen := heartbeat.NewGenerator(store, pub, logr.Discard(), 100*time.Millisecond) + + ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond) + defer cancel() + + err := gen.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + store.mu.Lock() + defer store.mu.Unlock() + Expect(store.updatedIDs).To(BeEmpty()) + }) + }) }) diff --git a/osac-metering/metering-service/internal/projection/postgres.go b/osac-metering/metering-service/internal/projection/postgres.go index 9e8379acf..5ed2e23bb 100644 --- a/osac-metering/metering-service/internal/projection/postgres.go +++ b/osac-metering/metering-service/internal/projection/postgres.go @@ -33,7 +33,7 @@ func (s *PostgresStore) Get(ctx context.Context, resourceID string) (*ResourceSt SELECT resource_id, resource_type, tenant_id, project_id, current_state, previous_state, is_billable, billable_since, last_heartbeat_at, transition_time, fulfillment_version, - billing_dimensions + billing_dimensions, component_billable_since FROM metering_resource_state WHERE resource_id = $1`, resourceID) @@ -53,6 +53,10 @@ func (s *PostgresStore) Upsert(ctx context.Context, state ResourceState) error { if err != nil { return fmt.Errorf("marshaling billing dimensions: %w", err) } + componentSince, err := json.Marshal(state.ComponentBillableSince) + if err != nil { + return fmt.Errorf("marshaling component billable since: %w", err) + } tx, err := s.pool.Begin(ctx) if err != nil { @@ -84,8 +88,8 @@ func (s *PostgresStore) Upsert(ctx context.Context, state ResourceState) error { resource_id, resource_type, tenant_id, project_id, current_state, previous_state, is_billable, billable_since, last_heartbeat_at, transition_time, fulfillment_version, - billing_dimensions, updated_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, NOW()) + billing_dimensions, component_billable_since, updated_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, NOW()) ON CONFLICT (resource_id) DO UPDATE SET resource_type = EXCLUDED.resource_type, tenant_id = EXCLUDED.tenant_id, @@ -98,6 +102,7 @@ func (s *PostgresStore) Upsert(ctx context.Context, state ResourceState) error { transition_time = EXCLUDED.transition_time, fulfillment_version = EXCLUDED.fulfillment_version, billing_dimensions = EXCLUDED.billing_dimensions, + component_billable_since = EXCLUDED.component_billable_since, updated_at = NOW() WHERE metering_resource_state.fulfillment_version <= EXCLUDED.fulfillment_version`, state.ResourceID, @@ -112,6 +117,7 @@ func (s *PostgresStore) Upsert(ctx context.Context, state ResourceState) error { state.TransitionTime, state.FulfillmentVersion, dimensions, + componentSince, ) if err != nil { return fmt.Errorf("upserting resource state %s: %w", state.ResourceID, err) @@ -135,7 +141,7 @@ func (s *PostgresStore) ListBillable(ctx context.Context) ([]ResourceState, erro SELECT resource_id, resource_type, tenant_id, project_id, current_state, previous_state, is_billable, billable_since, last_heartbeat_at, transition_time, fulfillment_version, - billing_dimensions + billing_dimensions, component_billable_since FROM metering_resource_state WHERE is_billable = TRUE`) if err != nil { @@ -150,7 +156,7 @@ func (s *PostgresStore) ListAll(ctx context.Context) ([]ResourceState, error) { SELECT resource_id, resource_type, tenant_id, project_id, current_state, previous_state, is_billable, billable_since, last_heartbeat_at, transition_time, fulfillment_version, - billing_dimensions + billing_dimensions, component_billable_since FROM metering_resource_state`) if err != nil { return nil, fmt.Errorf("querying all resources: %w", err) @@ -176,12 +182,13 @@ func (s *PostgresStore) UpdateLastHeartbeat(ctx context.Context, resourceIDs []s func scanResourceState(row pgx.Row) (*ResourceState, error) { var ( - state ResourceState - previousState *string - projectID *string - billableSince *time.Time - lastHeartbeat *time.Time - dimensionsJSON []byte + state ResourceState + previousState *string + projectID *string + billableSince *time.Time + lastHeartbeat *time.Time + dimensionsJSON []byte + componentSinceJSON []byte ) err := row.Scan( @@ -197,6 +204,7 @@ func scanResourceState(row pgx.Row) (*ResourceState, error) { &state.TransitionTime, &state.FulfillmentVersion, &dimensionsJSON, + &componentSinceJSON, ) if err != nil { return nil, err @@ -217,6 +225,12 @@ func scanResourceState(row pgx.Row) (*ResourceState, error) { } } + if len(componentSinceJSON) > 0 { + if err := json.Unmarshal(componentSinceJSON, &state.ComponentBillableSince); err != nil { + return nil, fmt.Errorf("unmarshaling component billable since: %w", err) + } + } + return &state, nil } diff --git a/osac-metering/metering-service/internal/projection/types.go b/osac-metering/metering-service/internal/projection/types.go index 8ead24b48..a119e608e 100644 --- a/osac-metering/metering-service/internal/projection/types.go +++ b/osac-metering/metering-service/internal/projection/types.go @@ -14,16 +14,23 @@ import ( ) type ResourceState struct { - ResourceID string - ResourceType string - TenantID string - ProjectID string - CurrentState string - PreviousState string - IsBillable bool - BillableSince *time.Time - LastHeartbeatAt *time.Time - TransitionTime time.Time - FulfillmentVersion int32 - BillingDimensions map[string]any + ResourceID string + ResourceType string + TenantID string + ProjectID string + CurrentState string + PreviousState string + IsBillable bool + BillableSince *time.Time + // ComponentBillableSince tracks, per node_set, when that component's + // billing dimensions last changed. N+1 decomposed resources (ClusterOrder) + // scale different node sets at different times — BillableSince alone + // would understate duration_seconds for a component that didn't cause + // the most recent reset. Keyed by ComponentRecord.NodeSet; absent for + // resource types that don't decompose into components. + ComponentBillableSince map[string]time.Time + LastHeartbeatAt *time.Time + TransitionTime time.Time + FulfillmentVersion int32 + BillingDimensions map[string]any } diff --git a/osac-metering/metering-service/internal/reconciliation/correction.go b/osac-metering/metering-service/internal/reconciliation/correction.go index 251832ee3..3d9eee6a0 100644 --- a/osac-metering/metering-service/internal/reconciliation/correction.go +++ b/osac-metering/metering-service/internal/reconciliation/correction.go @@ -10,7 +10,9 @@ in compliance with the License. You may obtain a copy of the License at package reconciliation import ( + "encoding/json" "fmt" + "hash/fnv" "time" cloudevents "github.com/cloudevents/sdk-go/v2" @@ -50,19 +52,60 @@ type correctionData struct { SchemaVersion string `json:"schema_version"` } -func correctionDescription(reason CorrectionReason) string { - switch reason { - case MissedCreation: - return "Resource found in fulfillment-service but missing from metering projection" - case StateDrift: - return "Resource state in fulfillment-service differs from metering projection" - case BillingDimensionsDrift: - return "Billing dimensions in fulfillment-service differ from metering projection" - case MissedDeletion: - return "Resource found in metering projection but missing from fulfillment-service" - default: - return fmt.Sprintf("unknown correction reason: %s", reason) +var correctionDescriptions = map[CorrectionReason]string{ + MissedCreation: "Resource found in fulfillment-service but missing from metering projection", + StateDrift: "Resource state in fulfillment-service differs from metering projection", + BillingDimensionsDrift: "Billing dimensions in fulfillment-service differ from metering projection", + MissedDeletion: "Resource found in metering projection but missing from fulfillment-service", +} + +func correctionDescription(reason CorrectionReason) (string, error) { + desc, ok := correctionDescriptions[reason] + if !ok { + return "", fmt.Errorf("unknown correction reason: %s", reason) + } + return desc, nil +} + +func buildCorrectionEvents( + resourceID, resourceType, tenantID, projectID string, + reason CorrectionReason, + projectionState, sourceState string, + billingDimensions map[string]any, + interval *AffectedInterval, + now time.Time, +) ([]cloudevents.Event, error) { + baseID := fmt.Sprintf("correction/%s/%s/%s/%s/%s", resourceID, reason, projectionState, sourceState, + correctionFingerprint(billingDimensions, interval)) + buildFn := func(dims map[string]any, eventID string) (cloudevents.Event, error) { + ce, err := buildCorrectionEvent(resourceID, resourceType, tenantID, projectID, + reason, projectionState, sourceState, dims, interval, now) + if err != nil { + return ce, err + } + ce.SetID(eventID) + return ce, nil } + + return events.BuildResourceEvents(resourceType, billingDimensions, baseID, buildFn) +} + +// correctionFingerprint discriminates corrections that share resourceID, +// reason, and states but describe different discrepancies (e.g. two separate +// billing_dimensions_drift detections for the same resource while it stays +// in the same state). Content-based rather than time-based: repeat detection +// of the SAME unresolved discrepancy across reconciliation cycles must keep +// producing the SAME ID so it dedups as the design intends ("duplicate +// corrections... are acceptable"), while a genuinely different discrepancy +// must not collide with a prior one and get silently dropped. +func correctionFingerprint(billingDimensions map[string]any, interval *AffectedInterval) string { + enc, _ := json.Marshal(struct { + Dims map[string]any `json:"dims"` + Interval *AffectedInterval `json:"interval,omitempty"` + }{billingDimensions, interval}) + h := fnv.New64a() + _, _ = h.Write(enc) + return fmt.Sprintf("%x", h.Sum64()) } func buildCorrectionEvent( @@ -76,17 +119,22 @@ func buildCorrectionEvent( ce := cloudevents.NewEvent() ce.SetID(uuid.NewString()) ce.SetSource("osac-metering/reconciler") - ce.SetType("osac.resource.correction.v1") + ce.SetType(events.EventCorrection) ce.SetTime(now) events.SetOSACExtensions(&ce, resourceID, resourceType, tenantID, projectID) + description, err := correctionDescription(reason) + if err != nil { + return ce, err + } + data := correctionData{ ResourceID: resourceID, ResourceType: resourceType, TenantID: tenantID, ProjectID: events.NilIfEmpty(projectID), Reason: reason, - Description: correctionDescription(reason), + Description: description, CorrectedState: events.NilIfEmpty(sourceState), PreviousStateProjection: events.NilIfEmpty(projectionState), ActualStateFromSource: events.NilIfEmpty(sourceState), diff --git a/osac-metering/metering-service/internal/reconciliation/correction_internal_test.go b/osac-metering/metering-service/internal/reconciliation/correction_internal_test.go new file mode 100644 index 000000000..731a65f8b --- /dev/null +++ b/osac-metering/metering-service/internal/reconciliation/correction_internal_test.go @@ -0,0 +1,177 @@ +/* +Copyright (c) 2026 Red Hat, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +in compliance with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 +*/ + +package reconciliation + +import ( + "testing" + "time" + + "github.com/osac-project/osac-metering/internal/events" + "github.com/osac-project/osac-metering/internal/projection" +) + +func TestCorrectionDescription(t *testing.T) { + tests := []struct { + reason CorrectionReason + expected string + }{ + {MissedCreation, "Resource found in fulfillment-service but missing from metering projection"}, + {StateDrift, "Resource state in fulfillment-service differs from metering projection"}, + {BillingDimensionsDrift, "Billing dimensions in fulfillment-service differ from metering projection"}, + {MissedDeletion, "Resource found in metering projection but missing from fulfillment-service"}, + } + + for _, tc := range tests { + t.Run(string(tc.reason), func(t *testing.T) { + desc, err := correctionDescription(tc.reason) + if err != nil { + t.Fatalf("unexpected error for reason %s: %v", tc.reason, err) + } + if desc != tc.expected { + t.Errorf("expected %q, got %q", tc.expected, desc) + } + }) + } +} + +func TestCorrectionDescriptionUnknownReason(t *testing.T) { + _, err := correctionDescription("unknown_reason") + if err == nil { + t.Fatal("expected error for unknown correction reason, got nil") + } + expected := "unknown correction reason: unknown_reason" + if err.Error() != expected { + t.Errorf("expected error %q, got %q", expected, err.Error()) + } +} + +func TestBuildSyntheticHeartbeatsStableIDAcrossRetryOfSameGap(t *testing.T) { + lastHeartbeat := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) + ps := projection.ResourceState{ + ResourceID: "res-1", + ResourceType: events.ResourceTypeComputeInstance, + LastHeartbeatAt: &lastHeartbeat, + BillingDimensions: map[string]any{"instance_type": "m5.large"}, + } + + firstAttempt := lastHeartbeat.Add(65 * time.Minute) + secondAttempt := lastHeartbeat.Add(125 * time.Minute) + + first, err := buildSyntheticHeartbeats(ps, firstAttempt) + if err != nil { + t.Fatalf("first attempt: unexpected error: %v", err) + } + second, err := buildSyntheticHeartbeats(ps, secondAttempt) + if err != nil { + t.Fatalf("second attempt: unexpected error: %v", err) + } + + if len(first) != 1 || len(second) != 1 { + t.Fatalf("expected 1 event per attempt, got %d and %d", len(first), len(second)) + } + if first[0].ID() != second[0].ID() { + t.Errorf("expected the same CloudEvent ID for two attempts at closing the same unresolved gap (LastHeartbeatAt unchanged), got %q and %q", first[0].ID(), second[0].ID()) + } +} + +func TestBuildSyntheticHeartbeatsNewIDOnceGapResolves(t *testing.T) { + // Same reconciliation run (same "now"); only LastHeartbeatAt differs. + // Isolates the ID's dependency on LastHeartbeatAt from any dependency on now. + now := time.Date(2026, 1, 1, 15, 0, 0, 0, time.UTC) + firstGap := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) + secondGap := time.Date(2026, 1, 1, 14, 0, 0, 0, time.UTC) + + psBefore := projection.ResourceState{ + ResourceID: "res-1", + ResourceType: events.ResourceTypeComputeInstance, + LastHeartbeatAt: &firstGap, + BillingDimensions: map[string]any{"instance_type": "m5.large"}, + } + psAfter := psBefore + psAfter.LastHeartbeatAt = &secondGap + + before, err := buildSyntheticHeartbeats(psBefore, now) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + after, err := buildSyntheticHeartbeats(psAfter, now) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if before[0].ID() == after[0].ID() { + t.Errorf("expected a different CloudEvent ID once LastHeartbeatAt advances to a new gap, both were %q", before[0].ID()) + } +} + +func TestBuildCorrectionEventsDifferentDimensionsGetDifferentIDs(t *testing.T) { + now := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) + dimsA := map[string]any{"instance_type": "m5.large"} + dimsB := map[string]any{"instance_type": "m5.xlarge"} + + a, err := buildCorrectionEvents("res-1", events.ResourceTypeComputeInstance, "tenant-1", "", + BillingDimensionsDrift, "RUNNING", "RUNNING", dimsA, nil, now) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + b, err := buildCorrectionEvents("res-1", events.ResourceTypeComputeInstance, "tenant-1", "", + BillingDimensionsDrift, "RUNNING", "RUNNING", dimsB, nil, now) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if a[0].ID() == b[0].ID() { + t.Errorf("two distinct billing_dimensions_drift corrections (different dimensions) for the same resource/state must not share a CloudEvent ID, both were %q — the second would be silently dropped by adapter-side ID dedup", a[0].ID()) + } +} + +func TestBuildCorrectionEventsSameDimensionsGetSameID(t *testing.T) { + now1 := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) + now2 := time.Date(2026, 1, 1, 13, 0, 0, 0, time.UTC) // a later reconciliation cycle re-detecting the same unresolved drift + dims := map[string]any{"instance_type": "m5.large"} + + a, err := buildCorrectionEvents("res-1", events.ResourceTypeComputeInstance, "tenant-1", "", + BillingDimensionsDrift, "RUNNING", "RUNNING", dims, nil, now1) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + b, err := buildCorrectionEvents("res-1", events.ResourceTypeComputeInstance, "tenant-1", "", + BillingDimensionsDrift, "RUNNING", "RUNNING", dims, nil, now2) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if a[0].ID() != b[0].ID() { + t.Errorf("repeat detection of the SAME unresolved drift across reconciliation cycles should dedup (design.md: duplicate corrections for the same state are acceptable/harmless), got %q and %q", a[0].ID(), b[0].ID()) + } +} + +func TestBuildSyntheticHeartbeatsFallsBackToBillableSinceWhenNeverHeartbeated(t *testing.T) { + billableSince := time.Date(2026, 1, 1, 9, 0, 0, 0, time.UTC) + ps := projection.ResourceState{ + ResourceID: "res-never-hb", + ResourceType: events.ResourceTypeComputeInstance, + BillableSince: &billableSince, + BillingDimensions: map[string]any{"instance_type": "m5.large"}, + } + + first, err := buildSyntheticHeartbeats(ps, billableSince.Add(65*time.Minute)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + second, err := buildSyntheticHeartbeats(ps, billableSince.Add(125*time.Minute)) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if first[0].ID() != second[0].ID() { + t.Errorf("expected a stable ID keyed off BillableSince when LastHeartbeatAt is nil, got %q and %q", first[0].ID(), second[0].ID()) + } +} diff --git a/osac-metering/metering-service/internal/reconciliation/reconciler.go b/osac-metering/metering-service/internal/reconciliation/reconciler.go index 74d0e0734..19c5884b9 100644 --- a/osac-metering/metering-service/internal/reconciliation/reconciler.go +++ b/osac-metering/metering-service/internal/reconciliation/reconciler.go @@ -18,7 +18,6 @@ import ( cloudevents "github.com/cloudevents/sdk-go/v2" "github.com/go-logr/logr" - "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "google.golang.org/grpc" @@ -49,12 +48,17 @@ var ( }) ) -type ComputeInstanceLister interface { +type ComputeInstancesClient interface { List(ctx context.Context, in *privatev1.ComputeInstancesListRequest, opts ...grpc.CallOption) (*privatev1.ComputeInstancesListResponse, error) } +type ClustersClient interface { + List(ctx context.Context, in *privatev1.ClustersListRequest, opts ...grpc.CallOption) (*privatev1.ClustersListResponse, error) +} + type Reconciler struct { - computeClient ComputeInstanceLister + computeClient ComputeInstancesClient + clusterClient ClustersClient store projection.Store publisher kafkapub.EventPublisher logger logr.Logger @@ -62,7 +66,8 @@ type Reconciler struct { } func NewReconciler( - computeClient ComputeInstanceLister, + computeClient ComputeInstancesClient, + clusterClient ClustersClient, store projection.Store, publisher kafkapub.EventPublisher, logger logr.Logger, @@ -70,6 +75,7 @@ func NewReconciler( ) *Reconciler { return &Reconciler{ computeClient: computeClient, + clusterClient: clusterClient, store: store, publisher: publisher, logger: logger, @@ -129,27 +135,40 @@ func (r *Reconciler) Reconcile(ctx context.Context) error { return nil } +func (r *Reconciler) publishCorrections(ctx context.Context, id, resourceType, tenantID, projectID string, reason CorrectionReason, projState, sourceState string, dims map[string]any, now time.Time) error { + ces, err := buildCorrectionEvents(id, resourceType, tenantID, projectID, + reason, projState, sourceState, dims, nil, now) + if err != nil { + return fmt.Errorf("building %s event for %s: %w", reason, id, err) + } + for _, ce := range ces { + if err := r.publisher.Publish(ctx, ce); err != nil { + return fmt.Errorf("publishing %s for %s: %w", reason, id, err) + } + } + reconCorrections.WithLabelValues(string(reason), resourceType).Inc() + return nil +} + func (r *Reconciler) reconcileFulfillmentResources(ctx context.Context, fulfillmentState map[string]fulfillmentResource, projMap map[string]projection.ResourceState, now time.Time) (int, error) { corrections := 0 for id, fs := range fulfillmentState { ps, exists := projMap[id] if !exists { - ce, ceErr := buildCorrectionEvent(id, "compute_instance", fs.tenantID, fs.projectID, - MissedCreation, "", fs.state, fs.billingDimensions, nil, now) - if ceErr != nil { - return corrections, fmt.Errorf("building missed_creation event for %s: %w", id, ceErr) - } - if err := r.publisher.Publish(ctx, ce); err != nil { - return corrections, fmt.Errorf("publishing missed_creation for %s: %w", id, err) + if err := r.publishCorrections(ctx, id, fs.resourceType, fs.tenantID, fs.projectID, + MissedCreation, "", fs.state, fs.billingDimensions, now); err != nil { + return corrections, err } - reconCorrections.WithLabelValues(string(MissedCreation), "compute_instance").Inc() corrections++ - isBillable := events.IsBillableState(fs.state) + isBillable, billErr := isBillableForType(fs.resourceType, fs.state) + if billErr != nil { + return corrections, fmt.Errorf("checking billability for %s: %w", id, billErr) + } newState := projection.ResourceState{ ResourceID: id, - ResourceType: "compute_instance", + ResourceType: fs.resourceType, TenantID: fs.tenantID, ProjectID: fs.projectID, CurrentState: fs.state, @@ -160,6 +179,7 @@ func (r *Reconciler) reconcileFulfillmentResources(ctx context.Context, fulfillm } if isBillable { newState.BillableSince = &now + newState.ComponentBillableSince = events.NextComponentBillableSince(nil, nil, fs.billingDimensions, now) } if err := r.store.Upsert(ctx, newState); err != nil { if errors.Is(err, projection.ErrStaleVersion) { @@ -190,18 +210,16 @@ func (r *Reconciler) reconcileFulfillmentResources(ctx context.Context, fulfillm } if ps.CurrentState != fs.state { - ce, ceErr := buildCorrectionEvent(id, "compute_instance", fs.tenantID, fs.projectID, - StateDrift, ps.CurrentState, fs.state, fs.billingDimensions, nil, now) - if ceErr != nil { - return corrections, fmt.Errorf("building state_drift event for %s: %w", id, ceErr) - } - if err := r.publisher.Publish(ctx, ce); err != nil { - return corrections, fmt.Errorf("publishing state_drift for %s: %w", id, err) + if err := r.publishCorrections(ctx, id, fs.resourceType, fs.tenantID, fs.projectID, + StateDrift, ps.CurrentState, fs.state, fs.billingDimensions, now); err != nil { + return corrections, err } - reconCorrections.WithLabelValues(string(StateDrift), "compute_instance").Inc() corrections++ - isBillable := events.IsBillableState(fs.state) + isBillable, billErr := isBillableForType(fs.resourceType, fs.state) + if billErr != nil { + return corrections, fmt.Errorf("checking billability for %s: %w", id, billErr) + } ps.PreviousState = ps.CurrentState ps.CurrentState = fs.state wasBillable := ps.IsBillable @@ -210,9 +228,11 @@ func (r *Reconciler) reconcileFulfillmentResources(ctx context.Context, fulfillm ps.TransitionTime = now if isBillable && !wasBillable { ps.BillableSince = &now + ps.ComponentBillableSince = events.NextComponentBillableSince(nil, nil, fs.billingDimensions, now) } if !isBillable { ps.BillableSince = nil + ps.ComponentBillableSince = nil } if err := r.store.Upsert(ctx, ps); err != nil { if errors.Is(err, projection.ErrStaleVersion) { @@ -222,15 +242,10 @@ func (r *Reconciler) reconcileFulfillmentResources(ctx context.Context, fulfillm } } } else if !events.DimensionsEqual(ps.BillingDimensions, fs.billingDimensions) { - ce, ceErr := buildCorrectionEvent(id, "compute_instance", fs.tenantID, fs.projectID, - BillingDimensionsDrift, ps.CurrentState, fs.state, fs.billingDimensions, nil, now) - if ceErr != nil { - return corrections, fmt.Errorf("building billing_dimensions_drift event for %s: %w", id, ceErr) + if err := r.publishCorrections(ctx, id, fs.resourceType, fs.tenantID, fs.projectID, + BillingDimensionsDrift, ps.CurrentState, fs.state, fs.billingDimensions, now); err != nil { + return corrections, err } - if err := r.publisher.Publish(ctx, ce); err != nil { - return corrections, fmt.Errorf("publishing billing_dimensions_drift for %s: %w", id, err) - } - reconCorrections.WithLabelValues(string(BillingDimensionsDrift), "compute_instance").Inc() corrections++ ps.BillingDimensions = fs.billingDimensions @@ -251,18 +266,21 @@ func (r *Reconciler) reconcileFulfillmentResources(ctx context.Context, fulfillm func (r *Reconciler) reconcileMissedDeletions(ctx context.Context, fulfillmentState map[string]fulfillmentResource, projMap map[string]projection.ResourceState, now time.Time) (int, error) { corrections := 0 + clusterSkipLogged := false for id, ps := range projMap { if _, exists := fulfillmentState[id]; !exists { - ce, ceErr := buildCorrectionEvent(id, ps.ResourceType, ps.TenantID, ps.ProjectID, - MissedDeletion, ps.CurrentState, "", ps.BillingDimensions, nil, now) - if ceErr != nil { - return corrections, fmt.Errorf("building missed_deletion event for %s: %w", id, ceErr) + if ps.ResourceType == events.ResourceTypeClusterOrder && r.clusterClient == nil { + if !clusterSkipLogged { + r.logger.Info("skipping cluster_order missed deletion checks, no cluster client configured") + clusterSkipLogged = true + } + continue } - if err := r.publisher.Publish(ctx, ce); err != nil { - return corrections, fmt.Errorf("publishing missed_deletion for %s: %w", id, err) + if err := r.publishCorrections(ctx, id, ps.ResourceType, ps.TenantID, ps.ProjectID, + MissedDeletion, ps.CurrentState, "", ps.BillingDimensions, now); err != nil { + return corrections, err } - reconCorrections.WithLabelValues(string(MissedDeletion), ps.ResourceType).Inc() corrections++ if err := r.store.Delete(ctx, id); err != nil { @@ -287,13 +305,20 @@ func (r *Reconciler) reconcileStaleHeartbeats(ctx context.Context, now time.Time for i := range freshProjection { ps := &freshProjection[i] if ps.LastHeartbeatAt == nil || now.Sub(*ps.LastHeartbeatAt) > 2*r.heartbeatInterval { - hbEvent, hbErr := buildSyntheticHeartbeat(*ps, now) + hbEvents, hbErr := buildSyntheticHeartbeats(*ps, now) if hbErr != nil { r.logger.Error(hbErr, "building synthetic heartbeat", "resource_id", ps.ResourceID) continue } - if err := r.publisher.Publish(ctx, hbEvent); err != nil { - r.logger.Error(err, "publishing synthetic heartbeat", "resource_id", ps.ResourceID) + published := true + for _, hb := range hbEvents { + if err := r.publisher.Publish(ctx, hb); err != nil { + r.logger.Error(err, "publishing synthetic heartbeat", "resource_id", ps.ResourceID) + published = false + break + } + } + if !published { continue } heartbeatIDs = append(heartbeatIDs, ps.ResourceID) @@ -330,6 +355,7 @@ func (r *Reconciler) RunPeriodic(ctx context.Context, interval time.Duration) { } type fulfillmentResource struct { + resourceType string state string version int32 tenantID string @@ -337,10 +363,36 @@ type fulfillmentResource struct { billingDimensions map[string]any } +var billabilityCheckers = map[string]func(string) bool{ + events.ResourceTypeComputeInstance: events.IsBillableState, + events.ResourceTypeClusterOrder: events.IsClusterBillableState, +} + +func isBillableForType(resourceType, state string) (bool, error) { + checker, ok := billabilityCheckers[resourceType] + if !ok { + return false, fmt.Errorf("unknown resource type: %s", resourceType) + } + return checker(state), nil +} + func (r *Reconciler) loadFulfillmentState(ctx context.Context) (map[string]fulfillmentResource, error) { result := make(map[string]fulfillmentResource) - var offset int32 + if err := r.loadComputeInstances(ctx, result); err != nil { + return nil, err + } + if r.clusterClient != nil { + if err := r.loadClusters(ctx, result); err != nil { + return nil, err + } + } + + return result, nil +} + +func (r *Reconciler) loadComputeInstances(ctx context.Context, result map[string]fulfillmentResource) error { + var offset int32 for { limit := int32(defaultPageSize) resp, err := r.computeClient.List(ctx, &privatev1.ComputeInstancesListRequest{ @@ -348,7 +400,7 @@ func (r *Reconciler) loadFulfillmentState(ctx context.Context) (map[string]fulfi Limit: &limit, }) if err != nil { - return nil, fmt.Errorf("listing compute instances (offset=%d): %w", offset, err) + return fmt.Errorf("listing compute instances (offset=%d): %w", offset, err) } items := resp.GetItems() @@ -366,6 +418,7 @@ func (r *Reconciler) loadFulfillmentState(ctx context.Context) (map[string]fulfi version = md.GetVersion() } result[ci.GetId()] = fulfillmentResource{ + resourceType: events.ResourceTypeComputeInstance, state: state, version: version, tenantID: tenantID, @@ -379,15 +432,83 @@ func (r *Reconciler) loadFulfillmentState(ctx context.Context) (map[string]fulfi } offset += int32(len(items)) } + return nil +} - return result, nil +func (r *Reconciler) loadClusters(ctx context.Context, result map[string]fulfillmentResource) error { + var offset int32 + for { + limit := int32(defaultPageSize) + resp, err := r.clusterClient.List(ctx, &privatev1.ClustersListRequest{ + Offset: &offset, + Limit: &limit, + }) + if err != nil { + return fmt.Errorf("listing clusters (offset=%d): %w", offset, err) + } + + items := resp.GetItems() + for _, cl := range items { + state := "UNSPECIFIED" + if s := cl.GetStatus(); s != nil { + state = strings.TrimPrefix(s.GetState().String(), events.ClusterStatePrefix) + } + tenantID := "" + projectID := "" + var version int32 + if md := cl.GetMetadata(); md != nil { + tenantID = md.GetTenant() + projectID = md.GetProject() + version = md.GetVersion() + } + result[cl.GetId()] = fulfillmentResource{ + resourceType: events.ResourceTypeClusterOrder, + state: state, + version: version, + tenantID: tenantID, + projectID: projectID, + billingDimensions: events.ClusterBillingDimensions(cl), + } + } + + if len(items) < defaultPageSize { + break + } + offset += int32(len(items)) + } + return nil +} + +func buildSyntheticHeartbeats(ps projection.ResourceState, now time.Time) ([]cloudevents.Event, error) { + baseID := fmt.Sprintf("synthetic-hb/%s/%d", ps.ResourceID, staleReferencePoint(ps, now).Unix()) + buildFn := func(dims map[string]any, eventID string) (cloudevents.Event, error) { + return buildSingleSyntheticHeartbeat(ps, dims, eventID, now) + } + + return events.BuildResourceEvents(ps.ResourceType, ps.BillingDimensions, baseID, buildFn) +} + +// staleReferencePoint returns the timestamp identifying the billing gap a +// synthetic heartbeat is catching up on, rather than the moment reconciliation +// happened to run. Keying the CloudEvent base ID off this value means retries +// across reconciliation cycles for the SAME unresolved gap reproduce the SAME +// ID (so adapter-side ID dedup recognizes them as the same logical catch-up), +// while a genuinely new gap (LastHeartbeatAt has since advanced) gets a new one. +func staleReferencePoint(ps projection.ResourceState, now time.Time) time.Time { + if ps.LastHeartbeatAt != nil { + return *ps.LastHeartbeatAt + } + if ps.BillableSince != nil { + return *ps.BillableSince + } + return now } -func buildSyntheticHeartbeat(ps projection.ResourceState, now time.Time) (cloudevents.Event, error) { +func buildSingleSyntheticHeartbeat(ps projection.ResourceState, billingDims map[string]any, eventID string, now time.Time) (cloudevents.Event, error) { ce := cloudevents.NewEvent() - ce.SetID(uuid.NewString()) + ce.SetID(eventID) ce.SetSource("osac-metering/reconciler") - ce.SetType("osac.resource.heartbeat.v1") + ce.SetType(events.EventHeartbeat) ce.SetTime(now) events.SetOSACExtensions(&ce, ps.ResourceID, ps.ResourceType, ps.TenantID, ps.ProjectID) @@ -403,7 +524,7 @@ func buildSyntheticHeartbeat(ps projection.ResourceState, now time.Time) (cloude "project_id": events.NilIfEmpty(ps.ProjectID), "current_state": ps.CurrentState, "duration_seconds": durationSeconds, - "billing_dimensions": ps.BillingDimensions, + "billing_dimensions": billingDims, "schema_version": "v1", } if err := ce.SetData(cloudevents.ApplicationJSON, data); err != nil { diff --git a/osac-metering/metering-service/internal/reconciliation/reconciler_test.go b/osac-metering/metering-service/internal/reconciliation/reconciler_test.go index daccc67cb..cbbce2ba5 100644 --- a/osac-metering/metering-service/internal/reconciliation/reconciler_test.go +++ b/osac-metering/metering-service/internal/reconciliation/reconciler_test.go @@ -14,6 +14,7 @@ import ( "google.golang.org/grpc" privatev1 "github.com/osac-project/osac-metering/internal/api/osac/private/v1" + "github.com/osac-project/osac-metering/internal/events" "github.com/osac-project/osac-metering/internal/projection" "github.com/osac-project/osac-metering/internal/reconciliation" ) @@ -41,6 +42,29 @@ func (m *mockComputeClient) List(_ context.Context, req *privatev1.ComputeInstan }, nil } +type mockClusterClient struct { + items []*privatev1.Cluster + err error +} + +func (m *mockClusterClient) List(_ context.Context, req *privatev1.ClustersListRequest, _ ...grpc.CallOption) (*privatev1.ClustersListResponse, error) { + if m.err != nil { + return nil, m.err + } + offset := int(req.GetOffset()) + limit := int(req.GetLimit()) + if offset >= len(m.items) { + return &privatev1.ClustersListResponse{}, nil + } + end := offset + limit + if end > len(m.items) { + end = len(m.items) + } + return &privatev1.ClustersListResponse{ + Items: m.items[offset:end], + }, nil +} + type mockStore struct { mu sync.Mutex states map[string]projection.ResourceState @@ -159,7 +183,7 @@ var _ = Describe("Reconciler", func() { } store := newMockStore() pub := &mockPublisher{} - recon := reconciliation.NewReconciler(client, store, pub, logr.Discard(), 60*time.Second) + recon := reconciliation.NewReconciler(client, nil, store, pub, logr.Discard(), 60*time.Second) Expect(recon.Reconcile(ctx)).To(Succeed()) @@ -167,7 +191,7 @@ var _ = Describe("Reconciler", func() { defer pub.mu.Unlock() var correctionFound bool for _, e := range pub.published { - if e.Type() == "osac.resource.correction.v1" { + if e.Type() == events.EventCorrection { var data map[string]any Expect(json.Unmarshal(e.Data(), &data)).To(Succeed()) Expect(data["reason"]).To(Equal("missed_creation")) @@ -187,12 +211,12 @@ var _ = Describe("Reconciler", func() { store := newMockStore() store.states["vm-gone"] = projection.ResourceState{ ResourceID: "vm-gone", - ResourceType: "compute_instance", + ResourceType: events.ResourceTypeComputeInstance, TenantID: "tenant-1", CurrentState: "RUNNING", } pub := &mockPublisher{} - recon := reconciliation.NewReconciler(client, store, pub, logr.Discard(), 60*time.Second) + recon := reconciliation.NewReconciler(client, nil, store, pub, logr.Discard(), 60*time.Second) Expect(recon.Reconcile(ctx)).To(Succeed()) @@ -217,13 +241,13 @@ var _ = Describe("Reconciler", func() { store := newMockStore() store.states["vm-drift"] = projection.ResourceState{ ResourceID: "vm-drift", - ResourceType: "compute_instance", + ResourceType: events.ResourceTypeComputeInstance, TenantID: "tenant-1", CurrentState: "RUNNING", FulfillmentVersion: 3, } pub := &mockPublisher{} - recon := reconciliation.NewReconciler(client, store, pub, logr.Discard(), 60*time.Second) + recon := reconciliation.NewReconciler(client, nil, store, pub, logr.Discard(), 60*time.Second) Expect(recon.Reconcile(ctx)).To(Succeed()) @@ -246,14 +270,14 @@ var _ = Describe("Reconciler", func() { store := newMockStore() store.states["res-drift"] = projection.ResourceState{ ResourceID: "res-drift", - ResourceType: "compute_instance", + ResourceType: events.ResourceTypeComputeInstance, TenantID: "tenant-1", CurrentState: "STOPPED", IsBillable: false, FulfillmentVersion: 1, } pub := &mockPublisher{} - recon := reconciliation.NewReconciler(client, store, pub, logr.Discard(), 60*time.Second) + recon := reconciliation.NewReconciler(client, nil, store, pub, logr.Discard(), 60*time.Second) Expect(recon.Reconcile(ctx)).To(Succeed()) @@ -275,7 +299,7 @@ var _ = Describe("Reconciler", func() { store := newMockStore() store.states["res-stale"] = projection.ResourceState{ ResourceID: "res-stale", - ResourceType: "compute_instance", + ResourceType: events.ResourceTypeComputeInstance, TenantID: "tenant-1", CurrentState: "RUNNING", IsBillable: true, @@ -283,7 +307,7 @@ var _ = Describe("Reconciler", func() { LastHeartbeatAt: &staleTime, } pub := &mockPublisher{} - recon := reconciliation.NewReconciler(client, store, pub, logr.Discard(), 60*time.Second) + recon := reconciliation.NewReconciler(client, nil, store, pub, logr.Discard(), 60*time.Second) Expect(recon.Reconcile(ctx)).To(Succeed()) @@ -291,7 +315,7 @@ var _ = Describe("Reconciler", func() { defer pub.mu.Unlock() found := false for _, e := range pub.published { - if e.Type() == "osac.resource.heartbeat.v1" { + if e.Type() == events.EventHeartbeat { found = true break } @@ -307,7 +331,7 @@ var _ = Describe("Reconciler", func() { } store := newMockStore() pub := &mockPublisher{} - recon := reconciliation.NewReconciler(client, store, pub, logr.Discard(), 60*time.Second) + recon := reconciliation.NewReconciler(client, nil, store, pub, logr.Discard(), 60*time.Second) Expect(recon.Reconcile(ctx)).To(Succeed()) @@ -329,7 +353,7 @@ var _ = Describe("Reconciler", func() { store := newMockStore() store.states["vm-ok"] = projection.ResourceState{ ResourceID: "vm-ok", - ResourceType: "compute_instance", + ResourceType: events.ResourceTypeComputeInstance, TenantID: "tenant-1", CurrentState: "RUNNING", BillingDimensions: map[string]any{ @@ -339,7 +363,7 @@ var _ = Describe("Reconciler", func() { }, } pub := &mockPublisher{} - recon := reconciliation.NewReconciler(client, store, pub, logr.Discard(), 60*time.Second) + recon := reconciliation.NewReconciler(client, nil, store, pub, logr.Discard(), 60*time.Second) Expect(recon.Reconcile(ctx)).To(Succeed()) @@ -352,7 +376,7 @@ var _ = Describe("Reconciler", func() { client := &mockComputeClient{err: fmt.Errorf("connection refused")} store := newMockStore() pub := &mockPublisher{} - recon := reconciliation.NewReconciler(client, store, pub, logr.Discard(), 60*time.Second) + recon := reconciliation.NewReconciler(client, nil, store, pub, logr.Discard(), 60*time.Second) err := recon.Reconcile(ctx) Expect(err).To(HaveOccurred()) @@ -367,7 +391,7 @@ var _ = Describe("Reconciler", func() { } store := newMockStore() pub := &mockPublisher{err: fmt.Errorf("kafka down")} - recon := reconciliation.NewReconciler(client, store, pub, logr.Discard(), 60*time.Second) + recon := reconciliation.NewReconciler(client, nil, store, pub, logr.Discard(), 60*time.Second) err := recon.Reconcile(ctx) Expect(err).To(HaveOccurred()) @@ -400,7 +424,7 @@ var _ = Describe("Reconciler", func() { store := newMockStore() store.states["vm-dims-drift"] = projection.ResourceState{ ResourceID: "vm-dims-drift", - ResourceType: "compute_instance", + ResourceType: events.ResourceTypeComputeInstance, TenantID: "tenant-1", CurrentState: "RUNNING", IsBillable: true, @@ -408,7 +432,7 @@ var _ = Describe("Reconciler", func() { BillingDimensions: map[string]any{"instance_type": "m5.large"}, } pub := &mockPublisher{} - recon := reconciliation.NewReconciler(client, store, pub, logr.Discard(), 60*time.Second) + recon := reconciliation.NewReconciler(client, nil, store, pub, logr.Discard(), 60*time.Second) Expect(recon.Reconcile(ctx)).To(Succeed()) @@ -416,7 +440,7 @@ var _ = Describe("Reconciler", func() { defer pub.mu.Unlock() var found bool for _, e := range pub.published { - if e.Type() == "osac.resource.correction.v1" { + if e.Type() == events.EventCorrection { var data map[string]any Expect(json.Unmarshal(e.Data(), &data)).To(Succeed()) if data["reason"] == "billing_dimensions_drift" { @@ -454,7 +478,7 @@ var _ = Describe("Reconciler", func() { store := newMockStore() store.states["vm-keep-billable"] = projection.ResourceState{ ResourceID: "vm-keep-billable", - ResourceType: "compute_instance", + ResourceType: events.ResourceTypeComputeInstance, TenantID: "tenant-1", CurrentState: "RUNNING", IsBillable: true, @@ -463,7 +487,7 @@ var _ = Describe("Reconciler", func() { BillingDimensions: map[string]any{"instance_type": "m5.large"}, } pub := &mockPublisher{} - recon := reconciliation.NewReconciler(client, store, pub, logr.Discard(), 60*time.Second) + recon := reconciliation.NewReconciler(client, nil, store, pub, logr.Discard(), 60*time.Second) Expect(recon.Reconcile(ctx)).To(Succeed()) @@ -484,7 +508,7 @@ var _ = Describe("Reconciler", func() { store := newMockStore() store.states["vm-version-advance"] = projection.ResourceState{ ResourceID: "vm-version-advance", - ResourceType: "compute_instance", + ResourceType: events.ResourceTypeComputeInstance, TenantID: "tenant-1", CurrentState: "RUNNING", FulfillmentVersion: 5, @@ -495,7 +519,7 @@ var _ = Describe("Reconciler", func() { }, } pub := &mockPublisher{} - recon := reconciliation.NewReconciler(client, store, pub, logr.Discard(), 60*time.Second) + recon := reconciliation.NewReconciler(client, nil, store, pub, logr.Discard(), 60*time.Second) Expect(recon.Reconcile(ctx)).To(Succeed()) @@ -532,7 +556,7 @@ var _ = Describe("Reconciler", func() { store := newMockStore() store.states["vm-match"] = projection.ResourceState{ ResourceID: "vm-match", - ResourceType: "compute_instance", + ResourceType: events.ResourceTypeComputeInstance, TenantID: "tenant-1", CurrentState: "RUNNING", BillingDimensions: map[string]any{ @@ -542,7 +566,7 @@ var _ = Describe("Reconciler", func() { }, } pub := &mockPublisher{} - recon := reconciliation.NewReconciler(client, store, pub, logr.Discard(), 60*time.Second) + recon := reconciliation.NewReconciler(client, nil, store, pub, logr.Discard(), 60*time.Second) Expect(recon.Reconcile(ctx)).To(Succeed()) @@ -561,7 +585,7 @@ var _ = Describe("Reconciler", func() { store := newMockStore() store.states["vm-stale"] = projection.ResourceState{ ResourceID: "vm-stale", - ResourceType: "compute_instance", + ResourceType: events.ResourceTypeComputeInstance, TenantID: "tenant-1", CurrentState: "RUNNING", FulfillmentVersion: 3, @@ -575,7 +599,7 @@ var _ = Describe("Reconciler", func() { "vm-stale": projection.ErrStaleVersion, } pub := &mockPublisher{} - recon := reconciliation.NewReconciler(client, store, pub, logr.Discard(), 60*time.Second) + recon := reconciliation.NewReconciler(client, nil, store, pub, logr.Discard(), 60*time.Second) Expect(recon.Reconcile(ctx)).To(Succeed()) @@ -592,7 +616,7 @@ var _ = Describe("Reconciler", func() { } store := newMockStore() pub := &mockPublisher{} - recon := reconciliation.NewReconciler(client, store, pub, logr.Discard(), 60*time.Second) + recon := reconciliation.NewReconciler(client, nil, store, pub, logr.Discard(), 60*time.Second) Expect(recon.Reconcile(ctx)).To(Succeed()) @@ -600,7 +624,7 @@ var _ = Describe("Reconciler", func() { defer pub.mu.Unlock() var found bool for _, e := range pub.published { - if e.Type() == "osac.resource.correction.v1" { + if e.Type() == events.EventCorrection { Expect(e.Extensions()["osacresourceid"]).To(Equal("vm-new")) found = true break @@ -615,7 +639,7 @@ var _ = Describe("Reconciler", func() { client := &mockComputeClient{} store := newMockStore() pub := &mockPublisher{} - recon := reconciliation.NewReconciler(client, store, pub, logr.Discard(), 60*time.Second) + recon := reconciliation.NewReconciler(client, nil, store, pub, logr.Discard(), 60*time.Second) periodicCtx, periodicCancel := context.WithCancel(ctx) done := make(chan struct{}) @@ -629,4 +653,266 @@ var _ = Describe("Reconciler", func() { Eventually(done, time.Second).Should(BeClosed()) }) }) + + Describe("CaaS cluster reconciliation", func() { + makeClusterProto := func(id, tenant string, state privatev1.ClusterState, version int32) *privatev1.Cluster { + return &privatev1.Cluster{ + Id: id, + Metadata: &privatev1.Metadata{ + Tenant: tenant, + Version: version, + }, + Spec: &privatev1.ClusterSpec{ + Template: &privatev1.ClusterTemplateReference{Name: "ocp-ci-small"}, + Version: &privatev1.ClusterVersionReference{Id: "4.17.0", Name: "4.17.0"}, + NodeSets: map[string]*privatev1.ClusterNodeSet{ + "gpu-workers": {HostType: &privatev1.HostTypeReference{Name: "gpu-h100"}, Size: 2}, + }, + }, + Status: &privatev1.ClusterStatus{State: state}, + } + } + + It("detects missed_creation for cluster and emits N+1 correction events", func() { + computeClient := &mockComputeClient{} + clusterClient := &mockClusterClient{ + items: []*privatev1.Cluster{ + makeClusterProto("cl-missed", "tenant-1", privatev1.ClusterState_CLUSTER_STATE_READY, 1), + }, + } + store := newMockStore() + pub := &mockPublisher{} + recon := reconciliation.NewReconciler(computeClient, clusterClient, store, pub, logr.Discard(), 60*time.Second) + + Expect(recon.Reconcile(ctx)).To(Succeed()) + + pub.mu.Lock() + defer pub.mu.Unlock() + correctionCount := 0 + for _, e := range pub.published { + if e.Type() == events.EventCorrection { + correctionCount++ + var data map[string]any + Expect(json.Unmarshal(e.Data(), &data)).To(Succeed()) + Expect(data["reason"]).To(Equal("missed_creation")) + Expect(data["resource_type"]).To(Equal(events.ResourceTypeClusterOrder)) + bd := data["billing_dimensions"].(map[string]any) + Expect(bd).To(HaveKey("component")) + Expect(bd).To(HaveKey("host_type")) + Expect(bd).NotTo(HaveKey("components")) + } + } + // 1 control_plane + 1 gpu-h100 worker = 2 correction events + Expect(correctionCount).To(Equal(2)) + + store.mu.Lock() + defer store.mu.Unlock() + Expect(store.states).To(HaveKey("cl-missed")) + Expect(store.states["cl-missed"].ResourceType).To(Equal(events.ResourceTypeClusterOrder)) + Expect(store.states["cl-missed"].IsBillable).To(BeTrue()) + }) + + It("detects state_drift for cluster and emits N+1 correction events", func() { + computeClient := &mockComputeClient{} + clusterClient := &mockClusterClient{ + items: []*privatev1.Cluster{ + makeClusterProto("cl-drift", "tenant-1", privatev1.ClusterState_CLUSTER_STATE_FAILED, 2), + }, + } + store := newMockStore() + now := time.Now().UTC().Truncate(time.Microsecond) + store.states["cl-drift"] = projection.ResourceState{ + ResourceID: "cl-drift", + ResourceType: events.ResourceTypeClusterOrder, + TenantID: "tenant-1", + CurrentState: "READY", + IsBillable: true, + BillableSince: &now, + FulfillmentVersion: 1, + BillingDimensions: map[string]any{ + "cluster_template": "ocp-ci-small", + "release_image": "4.17.0", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": float64(1)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": float64(2)}, + }, + }, + } + + pub := &mockPublisher{} + recon := reconciliation.NewReconciler(computeClient, clusterClient, store, pub, logr.Discard(), 60*time.Second) + + Expect(recon.Reconcile(ctx)).To(Succeed()) + + pub.mu.Lock() + defer pub.mu.Unlock() + driftCount := 0 + for _, e := range pub.published { + if e.Type() == events.EventCorrection { + var data map[string]any + Expect(json.Unmarshal(e.Data(), &data)).To(Succeed()) + if data["reason"] == "state_drift" { + driftCount++ + } + } + } + Expect(driftCount).To(Equal(2)) + + store.mu.Lock() + defer store.mu.Unlock() + Expect(store.states["cl-drift"].CurrentState).To(Equal("FAILED")) + Expect(store.states["cl-drift"].IsBillable).To(BeFalse()) + }) + + It("detects missed_deletion for cluster and emits N+1 correction events", func() { + computeClient := &mockComputeClient{} + clusterClient := &mockClusterClient{} + store := newMockStore() + now := time.Now().UTC().Truncate(time.Microsecond) + store.states["cl-gone"] = projection.ResourceState{ + ResourceID: "cl-gone", + ResourceType: events.ResourceTypeClusterOrder, + TenantID: "tenant-1", + CurrentState: "READY", + IsBillable: true, + BillableSince: &now, + BillingDimensions: map[string]any{ + "cluster_template": "ocp-ci-small", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": float64(1)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": float64(2)}, + }, + }, + } + + pub := &mockPublisher{} + recon := reconciliation.NewReconciler(computeClient, clusterClient, store, pub, logr.Discard(), 60*time.Second) + + Expect(recon.Reconcile(ctx)).To(Succeed()) + + pub.mu.Lock() + defer pub.mu.Unlock() + deletionCount := 0 + for _, e := range pub.published { + if e.Type() == events.EventCorrection { + var data map[string]any + Expect(json.Unmarshal(e.Data(), &data)).To(Succeed()) + if data["reason"] == "missed_deletion" { + deletionCount++ + bd := data["billing_dimensions"].(map[string]any) + Expect(bd).To(HaveKey("component")) + Expect(bd).NotTo(HaveKey("components")) + } + } + } + Expect(deletionCount).To(Equal(2)) + + store.mu.Lock() + defer store.mu.Unlock() + Expect(store.states).ToNot(HaveKey("cl-gone")) + }) + + It("skips cluster missed_deletion when clusterClient is nil", func() { + computeClient := &mockComputeClient{} + store := newMockStore() + now := time.Now().UTC().Truncate(time.Microsecond) + store.states["cl-safe"] = projection.ResourceState{ + ResourceID: "cl-safe", + ResourceType: events.ResourceTypeClusterOrder, + TenantID: "tenant-1", + CurrentState: "READY", + IsBillable: true, + BillableSince: &now, + LastHeartbeatAt: &now, + BillingDimensions: map[string]any{}, + } + + pub := &mockPublisher{} + recon := reconciliation.NewReconciler(computeClient, nil, store, pub, logr.Discard(), 60*time.Second) + + Expect(recon.Reconcile(ctx)).To(Succeed()) + + pub.mu.Lock() + defer pub.mu.Unlock() + for _, e := range pub.published { + Expect(e.Type()).ToNot(Equal(events.EventCorrection)) + } + + store.mu.Lock() + defer store.mu.Unlock() + Expect(store.states).To(HaveKey("cl-safe")) + }) + + It("paginates ListClusters correctly", func() { + clusters := make([]*privatev1.Cluster, 0, 600) + for i := range 600 { + clusters = append(clusters, makeClusterProto( + fmt.Sprintf("cl-%d", i), "tenant-1", + privatev1.ClusterState_CLUSTER_STATE_READY, 1)) + } + computeClient := &mockComputeClient{} + clusterClient := &mockClusterClient{items: clusters} + store := newMockStore() + pub := &mockPublisher{} + recon := reconciliation.NewReconciler(computeClient, clusterClient, store, pub, logr.Discard(), 60*time.Second) + + Expect(recon.Reconcile(ctx)).To(Succeed()) + + store.mu.Lock() + defer store.mu.Unlock() + Expect(store.states).To(HaveLen(600)) + }) + + It("produces deterministic synthetic heartbeat IDs for clusters", func() { + computeClient := &mockComputeClient{} + clusterClient := &mockClusterClient{ + items: []*privatev1.Cluster{ + makeClusterProto("cl-hb", "tenant-1", privatev1.ClusterState_CLUSTER_STATE_READY, 1), + }, + } + store := newMockStore() + now := time.Now().Add(-5 * time.Minute).UTC().Truncate(time.Microsecond) + store.states["cl-hb"] = projection.ResourceState{ + ResourceID: "cl-hb", + ResourceType: events.ResourceTypeClusterOrder, + TenantID: "tenant-1", + CurrentState: "READY", + IsBillable: true, + BillableSince: &now, + FulfillmentVersion: 1, + BillingDimensions: map[string]any{ + "cluster_template": "ocp-ci-small", + "release_image": "4.17.0", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": float64(1)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": float64(2)}, + }, + }, + } + + pub := &mockPublisher{} + recon := reconciliation.NewReconciler(computeClient, clusterClient, store, pub, logr.Discard(), 60*time.Second) + + Expect(recon.Reconcile(ctx)).To(Succeed()) + + pub.mu.Lock() + defer pub.mu.Unlock() + + var hbEvents []cloudevents.Event + for _, e := range pub.published { + if e.Type() == events.EventHeartbeat { + hbEvents = append(hbEvents, e) + } + } + Expect(hbEvents).To(HaveLen(2)) + + ids := map[string]bool{} + for _, e := range hbEvents { + Expect(e.ID()).To(ContainSubstring("synthetic-hb/cl-hb/")) + Expect(e.ID()).NotTo(ContainSubstring("synthetic-hb/cl-hb//")) + ids[e.ID()] = true + } + Expect(ids).To(HaveLen(2)) + }) + }) }) diff --git a/osac-metering/metering-service/internal/watch/consumer.go b/osac-metering/metering-service/internal/watch/consumer.go index 4268f8343..8b4055383 100644 --- a/osac-metering/metering-service/internal/watch/consumer.go +++ b/osac-metering/metering-service/internal/watch/consumer.go @@ -26,16 +26,22 @@ import ( "github.com/osac-project/osac-metering/internal/projection" ) -var watchReconnects = promauto.NewCounter(prometheus.CounterOpts{ - Name: "osac_metering_watch_stream_reconnects_total", - Help: "Total Watch stream reconnections", -}) +var ( + watchReconnects = promauto.NewCounter(prometheus.CounterOpts{ + Name: "osac_metering_watch_stream_reconnects_total", + Help: "Total Watch stream reconnections", + }) + eventsSkipped = promauto.NewCounterVec(prometheus.CounterOpts{ + Name: "osac_metering_events_skipped_total", + Help: "Watch events skipped due to unsupported type or data quality issues", + }, []string{"reason"}) +) const ( defaultInitialDelay = 1 * time.Second defaultMaxDelay = 30 * time.Second defaultHandlerRetries = 3 - computeInstanceFilter = "has(event.compute_instance)" + meteringFilter = "has(event.compute_instance) || has(event.cluster)" ) // Consumer connects to the fulfillment-service gRPC Watch stream, maps @@ -94,7 +100,7 @@ func (c *Consumer) Run(ctx context.Context) error { } func (c *Consumer) consumeStream(ctx context.Context) (int, error) { - filter := computeInstanceFilter + filter := meteringFilter stream, err := c.client.Watch(ctx, &privatev1.EventsWatchRequest{ Filter: &filter, }) @@ -139,27 +145,57 @@ func (c *Consumer) handleEvent(ctx context.Context, event *privatev1.Event) erro transitionTime, err := mapper.TransitionTime(event.GetType()) if err != nil { + if errors.Is(err, events.ErrUnsupportedEvent) { + eventsSkipped.WithLabelValues("unsupported_event_type").Inc() + c.logger.V(1).Info("skipping unsupported event type", + "event_id", event.GetId(), "resource_id", resourceID) + return nil + } + if errors.Is(err, events.ErrDataQuality) && existing != nil && existing.CurrentState == currentState { + c.logger.V(1).Info("skipping metadata-only update with no state change", + "event_id", event.GetId(), "resource_id", resourceID, "state", currentState) + return nil + } return err } - if c.shouldSkipUpdate(event, existing, currentState, dims, transitionTime, resourceID) { + if c.shouldSkipUpdate(ctx, event, existing, currentState, dims, version, transitionTime, resourceID) { return nil } stateCtx := c.buildStateContext(existing, isBillable, transitionTime, dims) - ce, err := events.MapWatchEvent(event, mapper, stateCtx) + eventDims := dims + if mapper.ResourceType() == events.ResourceTypeClusterOrder && + (event.GetType() == privatev1.EventType_EVENT_TYPE_OBJECT_CREATED || + event.GetType() == privatev1.EventType_EVENT_TYPE_OBJECT_DELETED) { + eventDims = topLevelDims(dims) + } + + ce, err := events.MapWatchEvent(event, mapper, stateCtx, eventDims) if err != nil { if errors.Is(err, events.ErrTransientState) { return c.handleTransientState(ctx, mapper, existing, version, transitionTime) } + if errors.Is(err, events.ErrSkipTransition) { + if existing != nil && !events.DimensionsEqual(existing.BillingDimensions, dims) { + return c.handleScalingEvent(ctx, event, mapper, existing, transitionTime, version, currentState, isBillable, dims) + } + c.logger.V(1).Info("non-billing state transition, updating projection only", + "resource_id", resourceID, "state", currentState) + projState := c.buildProjectionState(mapper, existing, transitionTime, version, currentState, isBillable, dims) + if upsertErr := c.store.Upsert(ctx, projState); upsertErr != nil && !errors.Is(upsertErr, projection.ErrStaleVersion) { + return fmt.Errorf("upserting projection for %s: %w", resourceID, upsertErr) + } + return nil + } return err } projState := c.buildProjectionState(mapper, existing, transitionTime, version, currentState, isBillable, dims) if event.GetType() == privatev1.EventType_EVENT_TYPE_OBJECT_DELETED { - if err := c.publishWithRetry(ctx, ce); err != nil { + if err := c.publishLifecycleEvents(ctx, ce, mapper, event.GetId()); err != nil { return err } if existing != nil { @@ -170,20 +206,29 @@ func (c *Consumer) handleEvent(ctx context.Context, event *privatev1.Event) erro return nil } - err = c.store.Upsert(ctx, projState) - if err != nil { + return c.publishAndUpsert(ctx, func() error { + return c.publishLifecycleEvents(ctx, ce, mapper, event.GetId()) + }, projState, resourceID) +} + +// publishAndUpsert publishes events first, then commits projection state. +// Publish-first ensures no data loss: if publish fails, projection is not +// committed, and replay retries the full publish. If upsert fails after +// successful publish, replay produces duplicate events (handled by adapter +// dedup via deterministic CloudEvent IDs). +func (c *Consumer) publishAndUpsert(ctx context.Context, publish func() error, state projection.ResourceState, resourceID string) error { + if err := publish(); err != nil { + return err + } + + if err := c.store.Upsert(ctx, state); err != nil { if errors.Is(err, projection.ErrStaleVersion) { c.logger.Info("stale version, skipping projection update", - "resource_id", resourceID, "version", version) + "resource_id", resourceID) return nil } return fmt.Errorf("upserting projection for %s: %w", resourceID, err) } - - if err := c.publishWithRetry(ctx, ce); err != nil { - return err - } - return nil } @@ -221,13 +266,144 @@ func (c *Consumer) handleTransientState( return nil } -func (c *Consumer) shouldSkipUpdate(event *privatev1.Event, existing *projection.ResourceState, currentState string, dims map[string]any, transitionTime time.Time, resourceID string) bool { +// DimComponents is the billing dimensions key for the nested components array. +const DimComponents = "components" + +func (c *Consumer) publishLifecycleEvents(ctx context.Context, baseCE *cloudevents.Event, mapper events.ResourceMapper, eventID string) error { + if baseCE.Type() == events.EventCreated || baseCE.Type() == events.EventDeleted { + return c.publishWithRetry(ctx, baseCE) + } + + decomposed, err := events.BuildResourceEvents(mapper.ResourceType(), mapper.BillingDimensionsMap(), eventID, func(dims map[string]any, compEventID string) (cloudevents.Event, error) { + return c.buildComponentEvent(baseCE, compEventID, dims) + }) + if err != nil { + return err + } + for i := range decomposed { + if err := c.publishWithRetry(ctx, &decomposed[i]); err != nil { + return err + } + } + return nil +} + +func (c *Consumer) handleScalingEvent(ctx context.Context, event *privatev1.Event, mapper events.ResourceMapper, existing *projection.ResourceState, transitionTime time.Time, version int32, currentState string, isBillable bool, dims map[string]any) error { + resourceID := mapper.ResourceID() + projState := c.buildProjectionState(mapper, existing, transitionTime, version, currentState, isBillable, dims) + stateCtx := c.buildStateContext(existing, isBillable, transitionTime, dims) + + return c.publishAndUpsert(ctx, func() error { + if mapper.ResourceType() == events.ResourceTypeClusterOrder { + changed, err := events.ChangedComponents(existing.BillingDimensions, dims) + if err != nil { + return err + } + if len(changed) == 0 { + c.logger.V(1).Info("non-component dimension change, projection updated", + "resource_id", resourceID) + return nil + } + for _, comp := range changed { + scalingCtx := &events.StateContext{ + PreviousState: stateCtx.PreviousState, + WasBillable: stateCtx.WasBillable, + } + if !comp.IsNew { + scalingCtx.DurationSeconds = c.componentDurationSeconds(existing, comp.NodeSet, transitionTime) + } + ce, ceErr := c.buildScalingEvent( + events.ComponentEventID(event.GetId(), comp), + mapper, comp.FlatBillingDimensions(), scalingCtx, transitionTime) + if ceErr != nil { + return ceErr + } + if err := c.publishWithRetry(ctx, &ce); err != nil { + return err + } + } + c.logger.Info("published scaling events", + "resource_id", resourceID, "changed_components", len(changed)) + return nil + } + // VMaaS: single updated.v1 + ce, ceErr := c.buildScalingEvent(event.GetId(), mapper, dims, stateCtx, transitionTime) + if ceErr != nil { + return ceErr + } + return c.publishWithRetry(ctx, &ce) + }, projState, resourceID) +} + +func topLevelDims(dims map[string]any) map[string]any { + flat := make(map[string]any, len(dims)) + for k, v := range dims { + if k != DimComponents { + flat[k] = v + } + } + return flat +} + +func (c *Consumer) buildComponentEvent(baseCE *cloudevents.Event, eventID string, dims map[string]any) (cloudevents.Event, error) { + ce := cloudevents.NewEvent() + ce.SetID(eventID) + ce.SetSource(baseCE.Source()) + ce.SetType(baseCE.Type()) + ce.SetTime(baseCE.Time()) + + for k, v := range baseCE.Extensions() { + ce.SetExtension(k, v) + } + + var baseData map[string]any + if err := baseCE.DataAs(&baseData); err != nil { + return ce, fmt.Errorf("reading base event data: %w", err) + } + if baseData == nil { + baseData = map[string]any{} + } + + baseData["billing_dimensions"] = dims + if err := ce.SetData(cloudevents.ApplicationJSON, baseData); err != nil { + return ce, fmt.Errorf("setting component event data: %w", err) + } + return ce, nil +} + +func (c *Consumer) buildScalingEvent(eventID string, mapper events.ResourceMapper, dims map[string]any, stateCtx *events.StateContext, transitionTime time.Time) (cloudevents.Event, error) { + ce := cloudevents.NewEvent() + ce.SetID(eventID) + ce.SetSource("osac-metering") + ce.SetType(events.EventUpdated) + ce.SetTime(transitionTime) + + projectID := "" + if p := mapper.ProjectID(); p != nil { + projectID = *p + } + events.SetOSACExtensions(&ce, mapper.ResourceID(), mapper.ResourceType(), mapper.TenantID(), projectID) + + data := events.BuildLifecycleData(mapper, dims, stateCtx.PreviousState, stateCtx.DurationSeconds, transitionTime) + if err := ce.SetData(cloudevents.ApplicationJSON, data); err != nil { + return ce, fmt.Errorf("setting scaling event data: %w", err) + } + return ce, nil +} +func (c *Consumer) shouldSkipUpdate(ctx context.Context, event *privatev1.Event, existing *projection.ResourceState, currentState string, dims map[string]any, version int32, transitionTime time.Time, resourceID string) bool { if event.GetType() != privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED || existing == nil { return false } if existing.CurrentState != currentState || !events.DimensionsEqual(existing.BillingDimensions, dims) { return false } + if version > existing.FulfillmentVersion { + existing.FulfillmentVersion = version + existing.TransitionTime = transitionTime.UTC() + if err := c.store.Upsert(ctx, *existing); err != nil && !errors.Is(err, projection.ErrStaleVersion) { + c.logger.Error(err, "failed to advance projection version", "resource_id", resourceID) + } + } if !existing.TransitionTime.Truncate(time.Microsecond).Equal(transitionTime.UTC().Truncate(time.Microsecond)) { c.logger.Info("skipping replayed event (upserted but likely unpublished)", "resource_id", resourceID, "state", currentState) @@ -263,10 +439,38 @@ func (c *Consumer) buildProjectionState(mapper events.ResourceMapper, existing * } else { projState.BillableSince = existing.BillableSince } + var oldDims map[string]any + var oldSince map[string]time.Time + if existing != nil { + oldDims = existing.BillingDimensions + oldSince = existing.ComponentBillableSince + } + projState.ComponentBillableSince = events.NextComponentBillableSince(oldDims, oldSince, dims, tt) } return projState } +// componentDurationSeconds returns how long a component's prior billing +// dimensions were in effect. Returns nil if no per-component timestamp is +// recorded for nodeSet — an honest "unknown" (the same signal already used +// for a genuinely new component) rather than guessing via the resource-wide +// BillableSince, which would silently reintroduce a narrower version of the +// cross-component bug this exists to fix. The only path that can leave an +// entry missing is a Reconciler correction that hasn't been updated to +// maintain ComponentBillableSince (see events.NextComponentBillableSince +// callers in the reconciliation package) — logged so an unexpected rate of +// occurrence is debuggable rather than silently absorbed. +func (c *Consumer) componentDurationSeconds(existing *projection.ResourceState, nodeSet string, transitionTime time.Time) *float64 { + since, ok := existing.ComponentBillableSince[nodeSet] + if !ok { + c.logger.V(1).Info("no per-component billable-since recorded, reporting nil duration_seconds", + "resource_id", existing.ResourceID, "node_set", nodeSet) + return nil + } + duration := transitionTime.Sub(since).Seconds() + return &duration +} + func (c *Consumer) buildStateContext(existing *projection.ResourceState, nowBillable bool, transitionTime time.Time, newDims map[string]any) *events.StateContext { if existing == nil { return &events.StateContext{} @@ -275,7 +479,6 @@ func (c *Consumer) buildStateContext(existing *projection.ResourceState, nowBill sc := &events.StateContext{ PreviousState: existing.CurrentState, WasBillable: existing.IsBillable, - NewDimensions: newDims, } if existing.IsBillable && existing.BillableSince != nil { diff --git a/osac-metering/metering-service/internal/watch/consumer_internal_test.go b/osac-metering/metering-service/internal/watch/consumer_internal_test.go new file mode 100644 index 000000000..b73b4352a --- /dev/null +++ b/osac-metering/metering-service/internal/watch/consumer_internal_test.go @@ -0,0 +1,39 @@ +/* +Copyright (c) 2026 Red Hat, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +in compliance with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 +*/ + +package watch + +import ( + "testing" + + cloudevents "github.com/cloudevents/sdk-go/v2" +) + +func TestBuildComponentEventHandlesNilBaseData(t *testing.T) { + c := &Consumer{} + baseCE := cloudevents.NewEvent() + baseCE.SetID("evt-1") + baseCE.SetSource("osac-metering") + baseCE.SetType("osac.resource.started.v1") + // No SetData call: the base event carries zero-length data, so DataAs + // leaves the target map nil without returning an error. + + ce, err := c.buildComponentEvent(&baseCE, "evt-1/node-a", map[string]any{"component": "worker"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + var data map[string]any + if err := ce.DataAs(&data); err != nil { + t.Fatalf("unexpected error reading component event data: %v", err) + } + if data["billing_dimensions"] == nil { + t.Errorf("expected billing_dimensions to be set on the component event even when the base event carried no data") + } +} diff --git a/osac-metering/metering-service/internal/watch/consumer_test.go b/osac-metering/metering-service/internal/watch/consumer_test.go index 5a44da8c9..9b36f83b0 100644 --- a/osac-metering/metering-service/internal/watch/consumer_test.go +++ b/osac-metering/metering-service/internal/watch/consumer_test.go @@ -16,6 +16,7 @@ import ( "google.golang.org/protobuf/types/known/timestamppb" privatev1 "github.com/osac-project/osac-metering/internal/api/osac/private/v1" + "github.com/osac-project/osac-metering/internal/events" "github.com/osac-project/osac-metering/internal/projection" "github.com/osac-project/osac-metering/internal/watch" ) @@ -260,7 +261,7 @@ var _ = Describe("Consumer", func() { pub.mu.Lock() defer pub.mu.Unlock() Expect(pub.published).To(HaveLen(1)) - Expect(pub.published[0].Type()).To(Equal("osac.resource.created.v1")) + Expect(pub.published[0].Type()).To(Equal(events.EventCreated)) }) It("stops gracefully on context cancellation", func() { @@ -372,7 +373,7 @@ var _ = Describe("Consumer", func() { Expect(pub.published[0].ID()).To(Equal("good-evt")) }) - It("sets compute_instance filter on watch request", func() { + It("sets metering filter on watch request", func() { blockingStream := &mockWatchStream{ctx: ctx} client.results = []mockStreamResult{{stream: blockingStream}} @@ -394,7 +395,7 @@ var _ = Describe("Consumer", func() { client.mu.Lock() defer client.mu.Unlock() Expect(client.calls).ToNot(BeEmpty()) - Expect(client.calls[0].GetFilter()).To(Equal("has(event.compute_instance)")) + Expect(client.calls[0].GetFilter()).To(Equal("has(event.compute_instance) || has(event.cluster)")) }) It("fails fast on unknown payload type and reconnects", func() { @@ -488,12 +489,133 @@ var _ = Describe("Consumer", func() { Expect(client.watchCallCount()).To(BeNumerically(">=", 2)) }) + It("skips OBJECT_SIGNALED without killing the stream", func() { + ciRunning := makeComputeInstance("vm-signaled", "tenant-1") + ciRunning.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_RUNNING + + stream := &mockWatchStream{ + responses: []*privatev1.EventsWatchResponse{ + makeResponse(&privatev1.Event{ + Id: "evt-signaled", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_SIGNALED, + Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ciRunning}, + }), + makeResponse(makeEvent("evt-after", privatev1.EventType_EVENT_TYPE_OBJECT_CREATED)), + }, + } + client.results = []mockStreamResult{{stream: stream}} + + pub := &mockPublisher{published: make([]cloudevents.Event, 0, 1), cancelFunc: cancel} + consumer := newConsumer(pub) + + err := consumer.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + // Single stream — no reconnect + Expect(client.watchCallCount()).To(Equal(1)) + + pub.mu.Lock() + defer pub.mu.Unlock() + Expect(pub.published).To(HaveLen(1)) + Expect(pub.published[0].Type()).To(Equal("osac.resource.created.v1")) + }) + + It("skips metadata-only update with no state_transition_time without killing the stream", func() { + store := newMockStore() + store.states["vm-meta"] = projection.ResourceState{ + ResourceID: "vm-meta", + ResourceType: "compute_instance", + TenantID: "tenant-1", + CurrentState: "STARTING", + } + + ciNoTimestamp := makeComputeInstance("vm-meta", "tenant-1") + ciNoTimestamp.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STARTING + ciNoTimestamp.Status.StateTransitionTime = nil + + ciRunning := makeComputeInstance("vm-meta", "tenant-1") + ciRunning.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_RUNNING + ciRunning.Metadata.Version = 2 + + stream := &mockWatchStream{ + responses: []*privatev1.EventsWatchResponse{ + makeResponse(&privatev1.Event{ + Id: "evt-meta-update", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ciNoTimestamp}, + }), + makeResponse(&privatev1.Event{ + Id: "evt-running", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ciRunning}, + }), + }, + } + client.results = []mockStreamResult{{stream: stream}} + + pub := &mockPublisher{published: make([]cloudevents.Event, 0, 1), cancelFunc: cancel} + consumer := newConsumerWithStore(pub, store) + + err := consumer.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + // Single stream — no reconnect + Expect(client.watchCallCount()).To(Equal(1)) + + pub.mu.Lock() + defer pub.mu.Unlock() + Expect(pub.published).To(HaveLen(1)) + Expect(pub.published[0].Type()).To(Equal("osac.resource.resumed.v1")) + }) + + It("fails fast on data quality error when state actually changed", func() { + store := newMockStore() + store.states["vm-dq"] = projection.ResourceState{ + ResourceID: "vm-dq", + ResourceType: "compute_instance", + TenantID: "tenant-1", + CurrentState: "RUNNING", + } + + ciStopped := makeComputeInstance("vm-dq", "tenant-1") + ciStopped.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_STOPPED + ciStopped.Status.StateTransitionTime = nil + + goodEvent := makeEvent("evt-after", privatev1.EventType_EVENT_TYPE_OBJECT_CREATED) + + stream1 := &mockWatchStream{ + responses: []*privatev1.EventsWatchResponse{ + makeResponse(&privatev1.Event{ + Id: "evt-dq", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ciStopped}, + }), + }, + } + stream2 := &mockWatchStream{ + responses: []*privatev1.EventsWatchResponse{makeResponse(goodEvent)}, + } + client.results = []mockStreamResult{ + {stream: stream1}, + {stream: stream2}, + } + + pub := &mockPublisher{published: make([]cloudevents.Event, 0, 1), cancelFunc: cancel} + consumer := newConsumerWithStore(pub, store) + + err := consumer.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + // Stream reconnected — real data quality issue, fail fast + Expect(client.watchCallCount()).To(BeNumerically(">=", 2)) + }) + It("skips same-state same-dimensions updates", func() { store := newMockStore() now := time.Now().UTC().Truncate(time.Microsecond) store.states["vm-1"] = projection.ResourceState{ ResourceID: "vm-1", - ResourceType: "compute_instance", + ResourceType: events.ResourceTypeComputeInstance, TenantID: "tenant-1", CurrentState: "RUNNING", IsBillable: true, @@ -529,7 +651,7 @@ var _ = Describe("Consumer", func() { now := time.Now().UTC().Truncate(time.Microsecond) store.states["vm-resume"] = projection.ResourceState{ ResourceID: "vm-resume", - ResourceType: "compute_instance", + ResourceType: events.ResourceTypeComputeInstance, TenantID: "tenant-1", CurrentState: "STOPPED", IsBillable: false, @@ -561,7 +683,7 @@ var _ = Describe("Consumer", func() { pub.mu.Lock() defer pub.mu.Unlock() Expect(pub.published).To(HaveLen(1)) - Expect(pub.published[0].Type()).To(Equal("osac.resource.resumed.v1")) + Expect(pub.published[0].Type()).To(Equal(events.EventResumed)) }) It("deletes projection on OBJECT_DELETED and publishes event", func() { @@ -569,7 +691,7 @@ var _ = Describe("Consumer", func() { now := time.Now().UTC().Truncate(time.Microsecond) store.states["vm-del"] = projection.ResourceState{ ResourceID: "vm-del", - ResourceType: "compute_instance", + ResourceType: events.ResourceTypeComputeInstance, TenantID: "tenant-1", CurrentState: "RUNNING", IsBillable: true, @@ -600,19 +722,19 @@ var _ = Describe("Consumer", func() { pub.mu.Lock() defer pub.mu.Unlock() Expect(pub.published).To(HaveLen(1)) - Expect(pub.published[0].Type()).To(Equal("osac.resource.deleted.v1")) + Expect(pub.published[0].Type()).To(Equal(events.EventDeleted)) store.mu.Lock() defer store.mu.Unlock() Expect(store.states).ToNot(HaveKey("vm-del")) }) - It("skips projection update on stale version and does not publish", func() { + It("publishes event but skips projection update on stale version", func() { store := newMockStore() now := time.Now().UTC().Truncate(time.Microsecond) store.states["vm-stale"] = projection.ResourceState{ ResourceID: "vm-stale", - ResourceType: "compute_instance", + ResourceType: events.ResourceTypeComputeInstance, TenantID: "tenant-1", CurrentState: "STOPPED", IsBillable: false, @@ -634,32 +756,27 @@ var _ = Describe("Consumer", func() { } client.results = []mockStreamResult{{stream: stream}} - pub := &mockPublisher{} + pub := &mockPublisher{published: make([]cloudevents.Event, 0, 1), cancelFunc: cancel} consumer := newConsumerWithStore(pub, store) - done := make(chan error, 1) - go func() { done <- consumer.Run(ctx) }() - - time.Sleep(50 * time.Millisecond) - cancel() - Eventually(done, time.Second).Should(Receive(BeNil())) + err := consumer.Run(ctx) + Expect(err).ToNot(HaveOccurred()) pub.mu.Lock() defer pub.mu.Unlock() - Expect(pub.published).To(BeEmpty()) + Expect(pub.published).To(HaveLen(1), "event published even when projection is stale") store.mu.Lock() defer store.mu.Unlock() Expect(store.states["vm-stale"].FulfillmentVersion).To(Equal(int32(10))) }) - It("closes billing interval and resets BillableSince on dimension change while billable", func() { + It("publishes updated.v1 and updates projection on dimension change while billable (RUNNING->RUNNING)", func() { store := newMockStore() originalStart := time.Now().Add(-1 * time.Hour).UTC().Truncate(time.Microsecond) - instanceType := "m5.large" store.states["vm-resize"] = projection.ResourceState{ ResourceID: "vm-resize", - ResourceType: "compute_instance", + ResourceType: events.ResourceTypeComputeInstance, TenantID: "tenant-1", CurrentState: "RUNNING", IsBillable: true, @@ -674,7 +791,6 @@ var _ = Describe("Consumer", func() { ci.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_RUNNING ci.Metadata.Version = 2 ci.Spec = &privatev1.ComputeInstanceSpec{InstanceType: &privatev1.InstanceTypeReference{Name: newType}} - _ = instanceType event := &privatev1.Event{ Id: "evt-resize", @@ -687,6 +803,8 @@ var _ = Describe("Consumer", func() { } client.results = []mockStreamResult{{stream: stream}} + // RUNNING->RUNNING is Skip; dimension change now publishes + // updated.v1 for VMaaS via BuildDimensionChangeEvents. pub := &mockPublisher{published: make([]cloudevents.Event, 0, 1), cancelFunc: cancel} consumer := newConsumerWithStore(pub, store) @@ -696,11 +814,12 @@ var _ = Describe("Consumer", func() { pub.mu.Lock() defer pub.mu.Unlock() Expect(pub.published).To(HaveLen(1)) + Expect(pub.published[0].Type()).To(Equal(events.EventUpdated)) var data map[string]any Expect(json.Unmarshal(pub.published[0].Data(), &data)).To(Succeed()) - Expect(data["duration_seconds"]).ToNot(BeNil()) - Expect(data["duration_seconds"]).To(BeNumerically(">", 0)) + bd := data["billing_dimensions"].(map[string]any) + Expect(bd["instance_type"]).To(Equal("m5.xlarge")) store.mu.Lock() defer store.mu.Unlock() @@ -710,12 +829,66 @@ var _ = Describe("Consumer", func() { Expect(updated.BillableSince.After(originalStart)).To(BeTrue()) }) + It("publishes updated.v1 when VMaaS billing dimensions change while RUNNING", func() { + store := newMockStore() + originalStart := time.Now().Add(-1 * time.Hour).UTC().Truncate(time.Microsecond) + store.states["vm-dim-change"] = projection.ResourceState{ + ResourceID: "vm-dim-change", + ResourceType: events.ResourceTypeComputeInstance, + TenantID: "tenant-1", + CurrentState: "RUNNING", + IsBillable: true, + BillableSince: &originalStart, + FulfillmentVersion: 1, + BillingDimensions: map[string]any{"instance_type": "m5.large"}, + TransitionTime: originalStart, + } + + ci := makeComputeInstance("vm-dim-change", "tenant-1") + ci.Status.State = privatev1.ComputeInstanceState_COMPUTE_INSTANCE_STATE_RUNNING + ci.Metadata.Version = 2 + ci.Spec = &privatev1.ComputeInstanceSpec{InstanceType: &privatev1.InstanceTypeReference{Name: "m5.xlarge"}} + + event := &privatev1.Event{ + Id: "evt-dim-change", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ci}, + } + + stream := &mockWatchStream{ + responses: []*privatev1.EventsWatchResponse{makeResponse(event)}, + } + client.results = []mockStreamResult{{stream: stream}} + + pub := &mockPublisher{published: make([]cloudevents.Event, 0, 1), cancelFunc: cancel} + consumer := newConsumerWithStore(pub, store) + + err := consumer.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + pub.mu.Lock() + defer pub.mu.Unlock() + Expect(pub.published).To(HaveLen(1)) + Expect(pub.published[0].Type()).To(Equal(events.EventUpdated)) + + var data map[string]any + Expect(json.Unmarshal(pub.published[0].Data(), &data)).To(Succeed()) + bd := data["billing_dimensions"].(map[string]any) + Expect(bd["instance_type"]).To(Equal("m5.xlarge")) + Expect(data["duration_seconds"]).ToNot(BeNil()) + + store.mu.Lock() + defer store.mu.Unlock() + updated := store.states["vm-dim-change"] + Expect(updated.BillingDimensions["instance_type"]).To(Equal("m5.xlarge")) + }) + It("preserves billing context through RUNNING→STOPPING→STOPPED sequence", func() { store := newMockStore() billableStart := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) store.states["vm-stop-seq"] = projection.ResourceState{ ResourceID: "vm-stop-seq", - ResourceType: "compute_instance", + ResourceType: events.ResourceTypeComputeInstance, TenantID: "tenant-1", CurrentState: "RUNNING", IsBillable: true, @@ -767,7 +940,7 @@ var _ = Describe("Consumer", func() { // STOPPING is transient — no CloudEvent published for it. // Only suspended.v1 for STOPPED should be published. Expect(pub.published).To(HaveLen(1)) - Expect(pub.published[0].Type()).To(Equal("osac.resource.suspended.v1")) + Expect(pub.published[0].Type()).To(Equal(events.EventSuspended)) // suspended.v1 should have duration_seconds = 3600 (1 hour from // BillableSince to STOPPED transition time), proving billing context @@ -807,4 +980,687 @@ var _ = Describe("Consumer", func() { Expect(store.states["vm-new"].BillableSince).ToNot(BeNil()) }) }) + + Describe("CaaS Cluster events", func() { + makeCluster := func(id, tenant string, state privatev1.ClusterState, nodeSets map[string]*privatev1.ClusterNodeSet) *privatev1.Cluster { + return &privatev1.Cluster{ + Id: id, + Metadata: &privatev1.Metadata{ + Tenant: tenant, + Version: 2, + CreationTimestamp: timestamppb.Now(), + }, + Spec: &privatev1.ClusterSpec{ + Template: &privatev1.ClusterTemplateReference{Name: "ocp-ci-small"}, + Version: &privatev1.ClusterVersionReference{Id: "4.17.0", Name: "4.17.0"}, + NodeSets: nodeSets, + }, + Status: &privatev1.ClusterStatus{ + State: state, + StateTransitionTime: timestamppb.Now(), + }, + } + } + + defaultNodeSets := func() map[string]*privatev1.ClusterNodeSet { + return map[string]*privatev1.ClusterNodeSet{ + "gpu-workers": {HostType: &privatev1.HostTypeReference{Name: "gpu-h100"}, Size: 2}, + "cpu-workers": {HostType: &privatev1.HostTypeReference{Name: "cpu-only"}, Size: 3}, + } + } + + clusterBillingDims := func() map[string]any { + return map[string]any{ + "cluster_template": "ocp-ci-small", + "release_image": "4.17.0", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + map[string]any{"node_set": "cpu-workers", "component": "worker", "host_type": "cpu-only", "node_count": int32(3)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + } + } + + It("publishes exactly 1 event for cluster CREATED (not N+1)", func() { + cl := makeCluster("cl-1", "tenant-1", privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, defaultNodeSets()) + event := &privatev1.Event{ + Id: "evt-create", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_CREATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + stream := &mockWatchStream{ + responses: []*privatev1.EventsWatchResponse{makeResponse(event)}, + } + client.results = []mockStreamResult{{stream: stream}} + + pub := &mockPublisher{published: make([]cloudevents.Event, 0, 1), cancelFunc: cancel} + consumer := newConsumer(pub) + + err := consumer.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + pub.mu.Lock() + defer pub.mu.Unlock() + Expect(pub.published).To(HaveLen(1)) + Expect(pub.published[0].Type()).To(Equal(events.EventCreated)) + Expect(pub.published[0].Extensions()["osacresourcetype"]).To(Equal(events.ResourceTypeClusterOrder)) + }) + + It("cluster created.v1 has flat billing_dimensions without components", func() { + cl := makeCluster("cl-flat", "tenant-1", privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, defaultNodeSets()) + event := &privatev1.Event{ + Id: "evt-flat-create", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_CREATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + stream := &mockWatchStream{ + responses: []*privatev1.EventsWatchResponse{makeResponse(event)}, + } + client.results = []mockStreamResult{{stream: stream}} + + pub := &mockPublisher{published: make([]cloudevents.Event, 0, 1), cancelFunc: cancel} + consumer := newConsumer(pub) + + err := consumer.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + pub.mu.Lock() + defer pub.mu.Unlock() + Expect(pub.published).To(HaveLen(1)) + Expect(pub.published[0].Type()).To(Equal(events.EventCreated)) + + var data map[string]any + Expect(json.Unmarshal(pub.published[0].Data(), &data)).To(Succeed()) + bd := data["billing_dimensions"].(map[string]any) + Expect(bd).To(HaveKey("cluster_template")) + Expect(bd).NotTo(HaveKey("components")) + }) + + It("cluster deleted.v1 has flat billing_dimensions without components", func() { + store := newMockStore() + now := time.Now().UTC().Truncate(time.Microsecond) + store.states["cl-flat-del"] = projection.ResourceState{ + ResourceID: "cl-flat-del", + ResourceType: events.ResourceTypeClusterOrder, + TenantID: "tenant-1", + CurrentState: "DELETING", + IsBillable: false, + FulfillmentVersion: 1, + BillingDimensions: clusterBillingDims(), + TransitionTime: now, + } + + cl := makeCluster("cl-flat-del", "tenant-1", privatev1.ClusterState_CLUSTER_STATE_DELETING, defaultNodeSets()) + cl.Metadata.DeletionTimestamp = timestamppb.Now() + event := &privatev1.Event{ + Id: "evt-flat-del", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_DELETED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + stream := &mockWatchStream{ + responses: []*privatev1.EventsWatchResponse{makeResponse(event)}, + } + client.results = []mockStreamResult{{stream: stream}} + + pub := &mockPublisher{published: make([]cloudevents.Event, 0, 1), cancelFunc: cancel} + consumer := newConsumerWithStore(pub, store) + + err := consumer.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + pub.mu.Lock() + defer pub.mu.Unlock() + Expect(pub.published).To(HaveLen(1)) + Expect(pub.published[0].Type()).To(Equal(events.EventDeleted)) + + var data map[string]any + Expect(json.Unmarshal(pub.published[0].Data(), &data)).To(Succeed()) + bd := data["billing_dimensions"].(map[string]any) + Expect(bd).To(HaveKey("cluster_template")) + Expect(bd).NotTo(HaveKey("components")) + }) + + It("publishes N+1 started.v1 events for new cluster PROGRESSING", func() { + cl := makeCluster("cl-start", "tenant-1", privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, defaultNodeSets()) + event := &privatev1.Event{ + Id: "evt-start", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + stream := &mockWatchStream{ + responses: []*privatev1.EventsWatchResponse{makeResponse(event)}, + } + client.results = []mockStreamResult{{stream: stream}} + + // 3 events: 1 control_plane + 2 workers + pub := &mockPublisher{published: make([]cloudevents.Event, 0, 3), cancelFunc: cancel} + consumer := newConsumer(pub) + + err := consumer.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + pub.mu.Lock() + defer pub.mu.Unlock() + Expect(pub.published).To(HaveLen(3)) + for _, e := range pub.published { + Expect(e.Type()).To(Equal(events.EventStarted)) + } + }) + + It("each decomposed event has distinct per-component billing_dimensions", func() { + cl := makeCluster("cl-decomp", "tenant-1", privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, defaultNodeSets()) + event := &privatev1.Event{ + Id: "evt-decomp", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + stream := &mockWatchStream{ + responses: []*privatev1.EventsWatchResponse{makeResponse(event)}, + } + client.results = []mockStreamResult{{stream: stream}} + + pub := &mockPublisher{published: make([]cloudevents.Event, 0, 3), cancelFunc: cancel} + consumer := newConsumer(pub) + + err := consumer.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + pub.mu.Lock() + defer pub.mu.Unlock() + Expect(pub.published).To(HaveLen(3)) + + components := map[string]bool{} + for _, e := range pub.published { + var data map[string]any + Expect(json.Unmarshal(e.Data(), &data)).To(Succeed()) + bd := data["billing_dimensions"].(map[string]any) + comp := bd["component"].(string) + ":" + bd["host_type"].(string) + components[comp] = true + Expect(bd).To(HaveKey("cluster_template")) + Expect(bd).To(HaveKey("node_count")) + Expect(bd).NotTo(HaveKey("components")) + } + Expect(components).To(HaveLen(3)) + Expect(components).To(HaveKey("control_plane:_control_plane")) + Expect(components).To(HaveKey("worker:cpu-only")) + Expect(components).To(HaveKey("worker:gpu-h100")) + }) + + It("each decomposed event has deterministic component-scoped ID", func() { + cl := makeCluster("cl-ids", "tenant-1", privatev1.ClusterState_CLUSTER_STATE_PROGRESSING, defaultNodeSets()) + event := &privatev1.Event{ + Id: "evt-ids", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + stream := &mockWatchStream{ + responses: []*privatev1.EventsWatchResponse{makeResponse(event)}, + } + client.results = []mockStreamResult{{stream: stream}} + + pub := &mockPublisher{published: make([]cloudevents.Event, 0, 3), cancelFunc: cancel} + consumer := newConsumer(pub) + + err := consumer.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + pub.mu.Lock() + defer pub.mu.Unlock() + ids := map[string]bool{} + for _, e := range pub.published { + Expect(e.ID()).To(ContainSubstring("evt-ids/")) + ids[e.ID()] = true + } + Expect(ids).To(HaveLen(3)) + }) + + It("skips publish on PROGRESSING→READY (both billable) but updates projection", func() { + store := newMockStore() + now := time.Now().UTC().Truncate(time.Microsecond) + store.states["cl-ready"] = projection.ResourceState{ + ResourceID: "cl-ready", + ResourceType: events.ResourceTypeClusterOrder, + TenantID: "tenant-1", + CurrentState: "PROGRESSING", + IsBillable: true, + BillableSince: &now, + FulfillmentVersion: 1, + BillingDimensions: clusterBillingDims(), + TransitionTime: now, + } + + cl := makeCluster("cl-ready", "tenant-1", privatev1.ClusterState_CLUSTER_STATE_READY, defaultNodeSets()) + clEvent := &privatev1.Event{ + Id: "evt-ready", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + stream := &mockWatchStream{ + responses: []*privatev1.EventsWatchResponse{makeResponse(clEvent)}, + } + client.results = []mockStreamResult{{stream: stream}} + + pub := &mockPublisher{} + consumer := newConsumerWithStore(pub, store) + + done := make(chan error, 1) + go func() { done <- consumer.Run(ctx) }() + + time.Sleep(50 * time.Millisecond) + cancel() + Eventually(done, time.Second).Should(Receive(BeNil())) + + pub.mu.Lock() + defer pub.mu.Unlock() + Expect(pub.published).To(BeEmpty()) + + store.mu.Lock() + defer store.mu.Unlock() + Expect(store.states["cl-ready"].CurrentState).To(Equal("READY")) + Expect(store.states["cl-ready"].IsBillable).To(BeTrue()) + Expect(store.states["cl-ready"].BillableSince).To(Equal(&now)) + }) + + It("publishes N+1 suspended.v1 on READY→FAILED", func() { + store := newMockStore() + now := time.Now().UTC().Truncate(time.Microsecond) + store.states["cl-fail"] = projection.ResourceState{ + ResourceID: "cl-fail", + ResourceType: events.ResourceTypeClusterOrder, + TenantID: "tenant-1", + CurrentState: "READY", + IsBillable: true, + BillableSince: &now, + FulfillmentVersion: 1, + BillingDimensions: clusterBillingDims(), + TransitionTime: now, + } + + cl := makeCluster("cl-fail", "tenant-1", privatev1.ClusterState_CLUSTER_STATE_FAILED, defaultNodeSets()) + event := &privatev1.Event{ + Id: "evt-fail", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + stream := &mockWatchStream{ + responses: []*privatev1.EventsWatchResponse{makeResponse(event)}, + } + client.results = []mockStreamResult{{stream: stream}} + + pub := &mockPublisher{published: make([]cloudevents.Event, 0, 3), cancelFunc: cancel} + consumer := newConsumerWithStore(pub, store) + + err := consumer.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + pub.mu.Lock() + defer pub.mu.Unlock() + Expect(pub.published).To(HaveLen(3)) + for _, e := range pub.published { + Expect(e.Type()).To(Equal(events.EventSuspended)) + } + + store.mu.Lock() + defer store.mu.Unlock() + Expect(store.states["cl-fail"].IsBillable).To(BeFalse()) + Expect(store.states["cl-fail"].BillableSince).To(BeNil()) + }) + + It("publishes updated.v1 only for changed component on scaling", func() { + store := newMockStore() + now := time.Now().Add(-1 * time.Hour).UTC().Truncate(time.Microsecond) + store.states["cl-scale"] = projection.ResourceState{ + ResourceID: "cl-scale", + ResourceType: events.ResourceTypeClusterOrder, + TenantID: "tenant-1", + CurrentState: "READY", + IsBillable: true, + BillableSince: &now, + FulfillmentVersion: 1, + BillingDimensions: clusterBillingDims(), + ComponentBillableSince: map[string]time.Time{ + "_control_plane": now, + "cpu-workers": now, + "gpu-workers": now, + }, + TransitionTime: now, + } + + // Scale gpu-h100 from 2 to 4, cpu-only stays at 3 + scaledNodeSets := map[string]*privatev1.ClusterNodeSet{ + "gpu-workers": {HostType: &privatev1.HostTypeReference{Name: "gpu-h100"}, Size: 4}, + "cpu-workers": {HostType: &privatev1.HostTypeReference{Name: "cpu-only"}, Size: 3}, + } + cl := makeCluster("cl-scale", "tenant-1", privatev1.ClusterState_CLUSTER_STATE_READY, scaledNodeSets) + event := &privatev1.Event{ + Id: "evt-scale", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + stream := &mockWatchStream{ + responses: []*privatev1.EventsWatchResponse{makeResponse(event)}, + } + client.results = []mockStreamResult{{stream: stream}} + + pub := &mockPublisher{published: make([]cloudevents.Event, 0, 1), cancelFunc: cancel} + consumer := newConsumerWithStore(pub, store) + + err := consumer.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + pub.mu.Lock() + defer pub.mu.Unlock() + Expect(pub.published).To(HaveLen(1)) + Expect(pub.published[0].Type()).To(Equal(events.EventUpdated)) + + var data map[string]any + Expect(json.Unmarshal(pub.published[0].Data(), &data)).To(Succeed()) + bd := data["billing_dimensions"].(map[string]any) + Expect(bd["host_type"]).To(Equal("gpu-h100")) + Expect(bd["node_count"]).To(BeNumerically("==", 4)) + Expect(data["duration_seconds"]).ToNot(BeNil()) + }) + + It("sets duration_seconds=nil for newly-added component (no prior billing interval)", func() { + store := newMockStore() + billableStart := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) + store.states["cl-add"] = projection.ResourceState{ + ResourceID: "cl-add", + ResourceType: events.ResourceTypeClusterOrder, + TenantID: "tenant-1", + CurrentState: "READY", + IsBillable: true, + BillableSince: &billableStart, + FulfillmentVersion: 1, + BillingDimensions: map[string]any{ + "cluster_template": "ocp-ci-small", + "release_image": "4.17.0", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + }, + }, + TransitionTime: billableStart, + } + + addedNodeSets := map[string]*privatev1.ClusterNodeSet{ + "tpu-workers": {HostType: &privatev1.HostTypeReference{Name: "tpu-v5"}, Size: 2}, + } + cl := makeCluster("cl-add", "tenant-1", privatev1.ClusterState_CLUSTER_STATE_READY, addedNodeSets) + event := &privatev1.Event{ + Id: "evt-add-component", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + stream := &mockWatchStream{ + responses: []*privatev1.EventsWatchResponse{makeResponse(event)}, + } + client.results = []mockStreamResult{{stream: stream}} + + pub := &mockPublisher{published: make([]cloudevents.Event, 0, 1), cancelFunc: cancel} + consumer := newConsumerWithStore(pub, store) + + err := consumer.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + pub.mu.Lock() + defer pub.mu.Unlock() + Expect(pub.published).To(HaveLen(1)) + + var data map[string]any + Expect(json.Unmarshal(pub.published[0].Data(), &data)).To(Succeed()) + bd := data["billing_dimensions"].(map[string]any) + Expect(bd["node_set"]).To(Equal("tpu-workers")) + Expect(data["duration_seconds"]).To(BeNil(), + "newly-added component has no prior billing interval, duration must be nil") + }) + + It("sets duration for modified component and nil for new component in same scaling event", func() { + store := newMockStore() + billableStart := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) + store.states["cl-mixed"] = projection.ResourceState{ + ResourceID: "cl-mixed", + ResourceType: events.ResourceTypeClusterOrder, + TenantID: "tenant-1", + CurrentState: "READY", + IsBillable: true, + BillableSince: &billableStart, + FulfillmentVersion: 1, + BillingDimensions: map[string]any{ + "cluster_template": "ocp-ci-small", + "release_image": "4.17.0", + "components": []any{ + map[string]any{"node_set": "_control_plane", "component": "control_plane", "host_type": "_control_plane", "node_count": int32(1)}, + map[string]any{"node_set": "gpu-workers", "component": "worker", "host_type": "gpu-h100", "node_count": int32(2)}, + }, + }, + ComponentBillableSince: map[string]time.Time{ + "_control_plane": billableStart, + "gpu-workers": billableStart, + }, + TransitionTime: billableStart, + } + + mixedNodeSets := map[string]*privatev1.ClusterNodeSet{ + "gpu-workers": {HostType: &privatev1.HostTypeReference{Name: "gpu-h100"}, Size: 4}, + "tpu-workers": {HostType: &privatev1.HostTypeReference{Name: "tpu-v5"}, Size: 2}, + } + cl := makeCluster("cl-mixed", "tenant-1", privatev1.ClusterState_CLUSTER_STATE_READY, mixedNodeSets) + event := &privatev1.Event{ + Id: "evt-mixed", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + stream := &mockWatchStream{ + responses: []*privatev1.EventsWatchResponse{makeResponse(event)}, + } + client.results = []mockStreamResult{{stream: stream}} + + pub := &mockPublisher{published: make([]cloudevents.Event, 0, 2), cancelFunc: cancel} + consumer := newConsumerWithStore(pub, store) + + err := consumer.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + pub.mu.Lock() + defer pub.mu.Unlock() + Expect(pub.published).To(HaveLen(2)) + + eventsByNodeSet := map[string]map[string]any{} + for _, e := range pub.published { + var data map[string]any + Expect(json.Unmarshal(e.Data(), &data)).To(Succeed()) + bd := data["billing_dimensions"].(map[string]any) + eventsByNodeSet[bd["node_set"].(string)] = data + } + + Expect(eventsByNodeSet).To(HaveKey("gpu-workers")) + Expect(eventsByNodeSet["gpu-workers"]["duration_seconds"]).ToNot(BeNil(), + "modified component should have duration_seconds (closes prior billing interval)") + + Expect(eventsByNodeSet).To(HaveKey("tpu-workers")) + Expect(eventsByNodeSet["tpu-workers"]["duration_seconds"]).To(BeNil(), + "newly-added component should have nil duration_seconds (no prior interval)") + }) + + It("computes duration_seconds from each component's own last change, not the cluster-wide reset, across two sequential scaling events", func() { + store := newMockStore() + t0 := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC) + t1 := t0.Add(1 * time.Hour) + t2 := t0.Add(3 * time.Hour) + + store.states["cl-staggered"] = projection.ResourceState{ + ResourceID: "cl-staggered", + ResourceType: events.ResourceTypeClusterOrder, + TenantID: "tenant-1", + CurrentState: "READY", + IsBillable: true, + BillableSince: &t0, + FulfillmentVersion: 1, + BillingDimensions: clusterBillingDims(), + ComponentBillableSince: map[string]time.Time{ + "_control_plane": t0, + "cpu-workers": t0, + "gpu-workers": t0, + }, + TransitionTime: t0, + } + + // T1: cpu-workers scales 3->5, gpu-workers stays at 2 (unchanged since T0). + clAtT1 := makeCluster("cl-staggered", "tenant-1", privatev1.ClusterState_CLUSTER_STATE_READY, map[string]*privatev1.ClusterNodeSet{ + "cpu-workers": {HostType: &privatev1.HostTypeReference{Name: "cpu-only"}, Size: 5}, + "gpu-workers": {HostType: &privatev1.HostTypeReference{Name: "gpu-h100"}, Size: 2}, + }) + clAtT1.Status.StateTransitionTime = timestamppb.New(t1) + eventT1 := &privatev1.Event{ + Id: "evt-t1", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_Cluster{Cluster: clAtT1}, + } + + // T2: gpu-workers scales 2->4, cpu-workers stays at 5 (unchanged since T1). + clAtT2 := makeCluster("cl-staggered", "tenant-1", privatev1.ClusterState_CLUSTER_STATE_READY, map[string]*privatev1.ClusterNodeSet{ + "cpu-workers": {HostType: &privatev1.HostTypeReference{Name: "cpu-only"}, Size: 5}, + "gpu-workers": {HostType: &privatev1.HostTypeReference{Name: "gpu-h100"}, Size: 4}, + }) + clAtT2.Status.StateTransitionTime = timestamppb.New(t2) + eventT2 := &privatev1.Event{ + Id: "evt-t2", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_Cluster{Cluster: clAtT2}, + } + + stream := &mockWatchStream{ + responses: []*privatev1.EventsWatchResponse{makeResponse(eventT1), makeResponse(eventT2)}, + } + client.results = []mockStreamResult{{stream: stream}} + + pub := &mockPublisher{published: make([]cloudevents.Event, 0, 2), cancelFunc: cancel} + consumer := newConsumerWithStore(pub, store) + + err := consumer.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + pub.mu.Lock() + defer pub.mu.Unlock() + Expect(pub.published).To(HaveLen(2)) + + eventsByNodeSet := map[string]map[string]any{} + for _, e := range pub.published { + var data map[string]any + Expect(json.Unmarshal(e.Data(), &data)).To(Succeed()) + bd := data["billing_dimensions"].(map[string]any) + eventsByNodeSet[bd["node_set"].(string)] = data + } + + Expect(eventsByNodeSet).To(HaveKey("cpu-workers")) + Expect(eventsByNodeSet["cpu-workers"]["duration_seconds"]).To(BeNumerically("~", t1.Sub(t0).Seconds(), 1), + "cpu-workers' first-ever change should close a 1-hour interval since T0") + + Expect(eventsByNodeSet).To(HaveKey("gpu-workers")) + Expect(eventsByNodeSet["gpu-workers"]["duration_seconds"]).To(BeNumerically("~", t2.Sub(t0).Seconds(), 1), + "gpu-workers was unchanged since T0, so its closed interval must span T0->T2 (3 hours), "+ + "not T1->T2 (2 hours) from the cluster-wide reset caused by cpu-workers' unrelated change") + }) + + It("advances projection version on same-state-same-dims update with higher version", func() { + store := newMockStore() + now := time.Now().UTC().Truncate(time.Microsecond) + store.states["vm-version"] = projection.ResourceState{ + ResourceID: "vm-version", + ResourceType: events.ResourceTypeComputeInstance, + TenantID: "tenant-1", + CurrentState: "RUNNING", + IsBillable: true, + FulfillmentVersion: 5, + BillingDimensions: map[string]any{}, + TransitionTime: now, + } + + ci := makeComputeInstance("vm-version", "tenant-1") + ci.Metadata.Version = 10 + ci.Status.StateTransitionTime = timestamppb.New(now) + event := &privatev1.Event{ + Id: "evt-version-bump", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_UPDATED, + Payload: &privatev1.Event_ComputeInstance{ComputeInstance: ci}, + } + + stream := &mockWatchStream{ + responses: []*privatev1.EventsWatchResponse{makeResponse(event)}, + } + client.results = []mockStreamResult{{stream: stream}} + + pub := &mockPublisher{cancelFunc: cancel} + consumer := newConsumerWithStore(pub, store) + + done := make(chan error, 1) + go func() { done <- consumer.Run(ctx) }() + time.Sleep(50 * time.Millisecond) + cancel() + Eventually(done, time.Second).Should(Receive(BeNil())) + + pub.mu.Lock() + defer pub.mu.Unlock() + Expect(pub.published).To(BeEmpty(), "same state+dims should not publish") + + store.mu.Lock() + defer store.mu.Unlock() + Expect(store.states["vm-version"].FulfillmentVersion).To(Equal(int32(10)), + "version should be advanced even when event is skipped") + }) + + It("publishes exactly 1 event for cluster DELETED (not N+1)", func() { + store := newMockStore() + now := time.Now().UTC().Truncate(time.Microsecond) + store.states["cl-del"] = projection.ResourceState{ + ResourceID: "cl-del", + ResourceType: events.ResourceTypeClusterOrder, + TenantID: "tenant-1", + CurrentState: "DELETING", + IsBillable: false, + FulfillmentVersion: 1, + BillingDimensions: clusterBillingDims(), + TransitionTime: now, + } + + cl := makeCluster("cl-del", "tenant-1", privatev1.ClusterState_CLUSTER_STATE_DELETING, defaultNodeSets()) + cl.Metadata.DeletionTimestamp = timestamppb.Now() + event := &privatev1.Event{ + Id: "evt-del", + Type: privatev1.EventType_EVENT_TYPE_OBJECT_DELETED, + Payload: &privatev1.Event_Cluster{Cluster: cl}, + } + + stream := &mockWatchStream{ + responses: []*privatev1.EventsWatchResponse{makeResponse(event)}, + } + client.results = []mockStreamResult{{stream: stream}} + + pub := &mockPublisher{published: make([]cloudevents.Event, 0, 1), cancelFunc: cancel} + consumer := newConsumerWithStore(pub, store) + + err := consumer.Run(ctx) + Expect(err).ToNot(HaveOccurred()) + + pub.mu.Lock() + defer pub.mu.Unlock() + Expect(pub.published).To(HaveLen(1)) + Expect(pub.published[0].Type()).To(Equal(events.EventDeleted)) + + store.mu.Lock() + defer store.mu.Unlock() + Expect(store.states).ToNot(HaveKey("cl-del")) + }) + }) })