From 3f62a2bc3c1dcffcdc4c2fdaf8bcf6f0ecef57f7 Mon Sep 17 00:00:00 2001 From: jnathangreeg Date: Tue, 12 May 2026 10:43:46 +0300 Subject: [PATCH 1/6] NAUT-1310: add SBOM RPCs to StorageService MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds four SBOM RPCs to support node-agent (workload) and kubevuln (registry) writing SBOMs directly to the backend, per design `naut-1310-storage-deprecation.md` §5.2 / §6 S1: - PutSBOM(unary) — upload SBOM ≤ 4 MiB - PutSBOMStream(client) — upload SBOM > 4 MiB, chunked - GetSBOM(unary) — probe (metadata_only=true) or fetch ≤ 4 MiB - GetSBOMStream(server) — fetch > 4 MiB, chunked Primary key on the wire is `(customer_guid, image_digest, syft_version)`, where `customer_guid` is supplied via the existing gRPC metadata header pattern used by `SendContainerProfile`. SBOMSource enum distinguishes workload / registry / host origin so a single Vulnerability Scanner pipeline downstream can route per-kind. Hand-written StorageClient wrappers added in `storageclient.go`. Unary methods mirror the existing pattern; streaming methods marshal the SBOMSyft proto, chunk at 1 MiB, and reassemble server-side. Makefile updated to also invoke `protoc-gen-go-grpc` — previously it ran only `protoc-gen-gogo`, leaving `storage_service_grpc.pb.go` stale on proto changes. Co-Authored-By: Claude Opus 4.7 (1M context) --- pkg/client/v1/proto/Makefile | 16 +- pkg/client/v1/proto/storage_service.pb.go | 789 ++++++++++++++++-- pkg/client/v1/proto/storage_service.proto | 169 ++++ .../v1/proto/storage_service_grpc.pb.go | 193 ++++- pkg/client/v1/storageclient.go | 189 +++++ pkg/client/v1/storageclient_test.go | 130 +++ 6 files changed, 1421 insertions(+), 65 deletions(-) diff --git a/pkg/client/v1/proto/Makefile b/pkg/client/v1/proto/Makefile index ff048fa..be9b795 100644 --- a/pkg/client/v1/proto/Makefile +++ b/pkg/client/v1/proto/Makefile @@ -1,7 +1,7 @@ .PHONY: proto install-deps help proto: - @echo "Generating gRPC code from proto files using protoc-gen-gogo..." + @echo "Generating message bindings with protoc-gen-gogo..." @cd ../../../.. && go mod vendor @protoc \ --proto_path=. \ @@ -9,20 +9,28 @@ proto: --gogo_out=. \ --gogo_opt=paths=source_relative \ storage_service.proto + @echo "Generating gRPC service stubs with protoc-gen-go-grpc..." + @protoc \ + --proto_path=. \ + --proto_path=../../../../vendor \ + --go-grpc_out=. \ + --go-grpc_opt=paths=source_relative \ + storage_service.proto + @cd ../../../.. && rm -rf vendor @echo "Proto generation complete!" @echo "" @echo "IMPORTANT: Commit the generated files (*.pb.go, *_grpc.pb.go) to the repository" - @echo " You can delete the vendor directory after generation" install-deps: @echo "Installing protoc dependencies..." go install github.com/gogo/protobuf/protoc-gen-gogo@latest + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest @echo "Dependencies installed!" help: @echo "Available targets:" - @echo " make install-deps - Install protoc-gen-gogo" - @echo " make proto - Generate Go code from proto files using gogo (runs go mod vendor first)" + @echo " make install-deps - Install protoc-gen-gogo + protoc-gen-go-grpc" + @echo " make proto - Generate Go code from proto files (messages via gogo, services via go-grpc)" @echo "" @echo "After running 'make proto', commit the generated files to git:" @echo " git add *.pb.go *_grpc.pb.go" diff --git a/pkg/client/v1/proto/storage_service.pb.go b/pkg/client/v1/proto/storage_service.pb.go index d7968c7..7e6388f 100644 --- a/pkg/client/v1/proto/storage_service.pb.go +++ b/pkg/client/v1/proto/storage_service.pb.go @@ -33,6 +33,8 @@ const ( ErrorCode_ERROR_CODE_PROFILE_NOT_FOUND ErrorCode = 5 ErrorCode_ERROR_CODE_INTERNAL_ERROR ErrorCode = 6 ErrorCode_ERROR_CODE_PULSAR_ERROR ErrorCode = 7 + ErrorCode_ERROR_CODE_SBOM_NOT_FOUND ErrorCode = 8 + ErrorCode_ERROR_CODE_SBOM_TOO_LARGE ErrorCode = 9 ) var ErrorCode_name = map[int32]string{ @@ -44,6 +46,8 @@ var ErrorCode_name = map[int32]string{ 5: "ERROR_CODE_PROFILE_NOT_FOUND", 6: "ERROR_CODE_INTERNAL_ERROR", 7: "ERROR_CODE_PULSAR_ERROR", + 8: "ERROR_CODE_SBOM_NOT_FOUND", + 9: "ERROR_CODE_SBOM_TOO_LARGE", } var ErrorCode_value = map[string]int32{ @@ -55,6 +59,8 @@ var ErrorCode_value = map[string]int32{ "ERROR_CODE_PROFILE_NOT_FOUND": 5, "ERROR_CODE_INTERNAL_ERROR": 6, "ERROR_CODE_PULSAR_ERROR": 7, + "ERROR_CODE_SBOM_NOT_FOUND": 8, + "ERROR_CODE_SBOM_TOO_LARGE": 9, } func (x ErrorCode) String() string { @@ -65,6 +71,41 @@ func (ErrorCode) EnumDescriptor() ([]byte, []int) { return fileDescriptor_3d90829bc66d9c54, []int{0} } +// SBOMSource identifies how an SBOM entered the backend. +type SBOMSource int32 + +const ( + SBOMSource_SBOM_SOURCE_UNSPECIFIED SBOMSource = 0 + // SBOM_SOURCE_WORKLOAD: generated by node-agent for an in-cluster workload image. + SBOMSource_SBOM_SOURCE_WORKLOAD SBOMSource = 1 + // SBOM_SOURCE_REGISTRY: generated by kubevuln for a registry image (Registry Epic). + SBOMSource_SBOM_SOURCE_REGISTRY SBOMSource = 2 + // SBOM_SOURCE_HOST: generated by the host scanner for an EC2 host snapshot. + SBOMSource_SBOM_SOURCE_HOST SBOMSource = 3 +) + +var SBOMSource_name = map[int32]string{ + 0: "SBOM_SOURCE_UNSPECIFIED", + 1: "SBOM_SOURCE_WORKLOAD", + 2: "SBOM_SOURCE_REGISTRY", + 3: "SBOM_SOURCE_HOST", +} + +var SBOMSource_value = map[string]int32{ + "SBOM_SOURCE_UNSPECIFIED": 0, + "SBOM_SOURCE_WORKLOAD": 1, + "SBOM_SOURCE_REGISTRY": 2, + "SBOM_SOURCE_HOST": 3, +} + +func (x SBOMSource) String() string { + return proto.EnumName(SBOMSource_name, int32(x)) +} + +func (SBOMSource) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_3d90829bc66d9c54, []int{1} +} + // SendContainerProfileRequest contains the container profile to be stored // customer_guid, cluster, host_type, and host_id are sent via gRPC metadata headers type SendContainerProfileRequest struct { @@ -167,7 +208,7 @@ func (m *SendContainerProfileResponse) GetErrorCode() ErrorCode { // GetProfileRequest requests an aggregated profile // customer_guid, cluster, host_type, and host_id are sent via gRPC metadata headers type GetProfileRequest struct { - // Kind specifies the type of profile: "applicationProfile" or "networkNeighborhood" + // Kind specifies the type of profile: "applicationProfile", "networkNeighborhood", or "containerProfile" Kind string `protobuf:"bytes,1,opt,name=kind,proto3" json:"kind,omitempty"` // Namespace of the workload (k8s scope identifier) Namespace string `protobuf:"bytes,2,opt,name=namespace,proto3" json:"namespace,omitempty"` @@ -632,8 +673,606 @@ func (m *ListNetworkNeighborhoodsResponse) GetCont() string { return "" } +// PutSBOMRequest uploads an SBOM blob plus the keys identifying it. +// customer_guid, cluster, host_type, and host_id are sent via gRPC metadata +// headers (see the AuthInterceptor on the server side). The unary path is +// intended for SBOMs up to ~4 MiB; use PutSBOMStream for larger payloads. +type PutSBOMRequest struct { + // image_digest is the SHA-256 digest of the scanned image (without the + // "sha256:" prefix). Part of the primary key. + ImageDigest string `protobuf:"bytes,1,opt,name=image_digest,json=imageDigest,proto3" json:"image_digest,omitempty"` + // syft_version is the version of syft that produced the blob. Part of the + // primary key — multiple syft versions can coexist for one + // (customer_guid, image_digest) during gradual fleet upgrades. + SyftVersion string `protobuf:"bytes,2,opt,name=syft_version,json=syftVersion,proto3" json:"syft_version,omitempty"` + // source identifies how this SBOM was produced. + Source SBOMSource `protobuf:"varint,3,opt,name=source,proto3,enum=storageserver.v1.SBOMSource" json:"source,omitempty"` + // sbom is the SBOM payload as a SBOMSyft K8s resource (the same shape + // produced by syft + v1beta1.StripSBOM in node-agent / kubevuln). + Sbom *v1beta1.SBOMSyft `protobuf:"bytes,4,opt,name=sbom,proto3" json:"sbom,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *PutSBOMRequest) Reset() { *m = PutSBOMRequest{} } +func (m *PutSBOMRequest) String() string { return proto.CompactTextString(m) } +func (*PutSBOMRequest) ProtoMessage() {} +func (*PutSBOMRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_3d90829bc66d9c54, []int{8} +} +func (m *PutSBOMRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_PutSBOMRequest.Unmarshal(m, b) +} +func (m *PutSBOMRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_PutSBOMRequest.Marshal(b, m, deterministic) +} +func (m *PutSBOMRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_PutSBOMRequest.Merge(m, src) +} +func (m *PutSBOMRequest) XXX_Size() int { + return xxx_messageInfo_PutSBOMRequest.Size(m) +} +func (m *PutSBOMRequest) XXX_DiscardUnknown() { + xxx_messageInfo_PutSBOMRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_PutSBOMRequest proto.InternalMessageInfo + +func (m *PutSBOMRequest) GetImageDigest() string { + if m != nil { + return m.ImageDigest + } + return "" +} + +func (m *PutSBOMRequest) GetSyftVersion() string { + if m != nil { + return m.SyftVersion + } + return "" +} + +func (m *PutSBOMRequest) GetSource() SBOMSource { + if m != nil { + return m.Source + } + return SBOMSource_SBOM_SOURCE_UNSPECIFIED +} + +func (m *PutSBOMRequest) GetSbom() *v1beta1.SBOMSyft { + if m != nil { + return m.Sbom + } + return nil +} + +// PutSBOMResponse indicates success or failure of the operation. S3 + +// Postgres persistence happens asynchronously via Pulsar; success here +// means the upload was accepted and the Pulsar message was published. +type PutSBOMResponse struct { + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + ErrorMessage string `protobuf:"bytes,2,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + ErrorCode ErrorCode `protobuf:"varint,3,opt,name=error_code,json=errorCode,proto3,enum=storageserver.v1.ErrorCode" json:"error_code,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *PutSBOMResponse) Reset() { *m = PutSBOMResponse{} } +func (m *PutSBOMResponse) String() string { return proto.CompactTextString(m) } +func (*PutSBOMResponse) ProtoMessage() {} +func (*PutSBOMResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_3d90829bc66d9c54, []int{9} +} +func (m *PutSBOMResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_PutSBOMResponse.Unmarshal(m, b) +} +func (m *PutSBOMResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_PutSBOMResponse.Marshal(b, m, deterministic) +} +func (m *PutSBOMResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_PutSBOMResponse.Merge(m, src) +} +func (m *PutSBOMResponse) XXX_Size() int { + return xxx_messageInfo_PutSBOMResponse.Size(m) +} +func (m *PutSBOMResponse) XXX_DiscardUnknown() { + xxx_messageInfo_PutSBOMResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_PutSBOMResponse proto.InternalMessageInfo + +func (m *PutSBOMResponse) GetSuccess() bool { + if m != nil { + return m.Success + } + return false +} + +func (m *PutSBOMResponse) GetErrorMessage() string { + if m != nil { + return m.ErrorMessage + } + return "" +} + +func (m *PutSBOMResponse) GetErrorCode() ErrorCode { + if m != nil { + return m.ErrorCode + } + return ErrorCode_ERROR_CODE_UNSPECIFIED +} + +// PutSBOMChunk is a single chunk of a streamed SBOM upload. The first +// chunk in the stream MUST set metadata. Subsequent chunks set only +// blob_chunk. Concatenated blob_chunk values form the serialized SBOMSyft +// proto payload. +type PutSBOMChunk struct { + // metadata is set on the first chunk only. + Metadata *PutSBOMChunkMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // blob_chunk is a slice of the serialized SBOMSyft payload. + BlobChunk []byte `protobuf:"bytes,2,opt,name=blob_chunk,json=blobChunk,proto3" json:"blob_chunk,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *PutSBOMChunk) Reset() { *m = PutSBOMChunk{} } +func (m *PutSBOMChunk) String() string { return proto.CompactTextString(m) } +func (*PutSBOMChunk) ProtoMessage() {} +func (*PutSBOMChunk) Descriptor() ([]byte, []int) { + return fileDescriptor_3d90829bc66d9c54, []int{10} +} +func (m *PutSBOMChunk) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_PutSBOMChunk.Unmarshal(m, b) +} +func (m *PutSBOMChunk) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_PutSBOMChunk.Marshal(b, m, deterministic) +} +func (m *PutSBOMChunk) XXX_Merge(src proto.Message) { + xxx_messageInfo_PutSBOMChunk.Merge(m, src) +} +func (m *PutSBOMChunk) XXX_Size() int { + return xxx_messageInfo_PutSBOMChunk.Size(m) +} +func (m *PutSBOMChunk) XXX_DiscardUnknown() { + xxx_messageInfo_PutSBOMChunk.DiscardUnknown(m) +} + +var xxx_messageInfo_PutSBOMChunk proto.InternalMessageInfo + +func (m *PutSBOMChunk) GetMetadata() *PutSBOMChunkMetadata { + if m != nil { + return m.Metadata + } + return nil +} + +func (m *PutSBOMChunk) GetBlobChunk() []byte { + if m != nil { + return m.BlobChunk + } + return nil +} + +// PutSBOMChunkMetadata is the metadata header sent on the first chunk of a +// PutSBOMStream call. +type PutSBOMChunkMetadata struct { + ImageDigest string `protobuf:"bytes,1,opt,name=image_digest,json=imageDigest,proto3" json:"image_digest,omitempty"` + SyftVersion string `protobuf:"bytes,2,opt,name=syft_version,json=syftVersion,proto3" json:"syft_version,omitempty"` + Source SBOMSource `protobuf:"varint,3,opt,name=source,proto3,enum=storageserver.v1.SBOMSource" json:"source,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *PutSBOMChunkMetadata) Reset() { *m = PutSBOMChunkMetadata{} } +func (m *PutSBOMChunkMetadata) String() string { return proto.CompactTextString(m) } +func (*PutSBOMChunkMetadata) ProtoMessage() {} +func (*PutSBOMChunkMetadata) Descriptor() ([]byte, []int) { + return fileDescriptor_3d90829bc66d9c54, []int{11} +} +func (m *PutSBOMChunkMetadata) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_PutSBOMChunkMetadata.Unmarshal(m, b) +} +func (m *PutSBOMChunkMetadata) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_PutSBOMChunkMetadata.Marshal(b, m, deterministic) +} +func (m *PutSBOMChunkMetadata) XXX_Merge(src proto.Message) { + xxx_messageInfo_PutSBOMChunkMetadata.Merge(m, src) +} +func (m *PutSBOMChunkMetadata) XXX_Size() int { + return xxx_messageInfo_PutSBOMChunkMetadata.Size(m) +} +func (m *PutSBOMChunkMetadata) XXX_DiscardUnknown() { + xxx_messageInfo_PutSBOMChunkMetadata.DiscardUnknown(m) +} + +var xxx_messageInfo_PutSBOMChunkMetadata proto.InternalMessageInfo + +func (m *PutSBOMChunkMetadata) GetImageDigest() string { + if m != nil { + return m.ImageDigest + } + return "" +} + +func (m *PutSBOMChunkMetadata) GetSyftVersion() string { + if m != nil { + return m.SyftVersion + } + return "" +} + +func (m *PutSBOMChunkMetadata) GetSource() SBOMSource { + if m != nil { + return m.Source + } + return SBOMSource_SBOM_SOURCE_UNSPECIFIED +} + +// GetSBOMRequest probes for or fetches an SBOM by primary key. +// customer_guid is sent via gRPC metadata headers. +type GetSBOMRequest struct { + // image_digest is the SHA-256 digest of the requested image (without the + // "sha256:" prefix). + ImageDigest string `protobuf:"bytes,1,opt,name=image_digest,json=imageDigest,proto3" json:"image_digest,omitempty"` + // syft_version is the syft version that produced the requested SBOM. The + // (image_digest, syft_version) pair is the lookup key. Leave empty to + // request "latest available version" semantics; the server returns the + // most recently created SBOM for the image. + SyftVersion string `protobuf:"bytes,2,opt,name=syft_version,json=syftVersion,proto3" json:"syft_version,omitempty"` + // metadata_only skips the blob and returns only SBOMMetadata. Used by + // agents to decide whether to regenerate. + MetadataOnly bool `protobuf:"varint,3,opt,name=metadata_only,json=metadataOnly,proto3" json:"metadata_only,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetSBOMRequest) Reset() { *m = GetSBOMRequest{} } +func (m *GetSBOMRequest) String() string { return proto.CompactTextString(m) } +func (*GetSBOMRequest) ProtoMessage() {} +func (*GetSBOMRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_3d90829bc66d9c54, []int{12} +} +func (m *GetSBOMRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetSBOMRequest.Unmarshal(m, b) +} +func (m *GetSBOMRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetSBOMRequest.Marshal(b, m, deterministic) +} +func (m *GetSBOMRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetSBOMRequest.Merge(m, src) +} +func (m *GetSBOMRequest) XXX_Size() int { + return xxx_messageInfo_GetSBOMRequest.Size(m) +} +func (m *GetSBOMRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetSBOMRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetSBOMRequest proto.InternalMessageInfo + +func (m *GetSBOMRequest) GetImageDigest() string { + if m != nil { + return m.ImageDigest + } + return "" +} + +func (m *GetSBOMRequest) GetSyftVersion() string { + if m != nil { + return m.SyftVersion + } + return "" +} + +func (m *GetSBOMRequest) GetMetadataOnly() bool { + if m != nil { + return m.MetadataOnly + } + return false +} + +// GetSBOMResponse returns the SBOM (or just metadata). +type GetSBOMResponse struct { + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + ErrorMessage string `protobuf:"bytes,2,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + ErrorCode ErrorCode `protobuf:"varint,3,opt,name=error_code,json=errorCode,proto3,enum=storageserver.v1.ErrorCode" json:"error_code,omitempty"` + // exists indicates whether an SBOM row was found for the requested key. + // When false, success is still true and metadata + sbom are empty. + Exists bool `protobuf:"varint,4,opt,name=exists,proto3" json:"exists,omitempty"` + // metadata is populated whenever exists=true. + Metadata *SBOMMetadata `protobuf:"bytes,5,opt,name=metadata,proto3" json:"metadata,omitempty"` + // sbom is populated only when exists=true and metadata_only was false on + // the request. + Sbom *v1beta1.SBOMSyft `protobuf:"bytes,6,opt,name=sbom,proto3" json:"sbom,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetSBOMResponse) Reset() { *m = GetSBOMResponse{} } +func (m *GetSBOMResponse) String() string { return proto.CompactTextString(m) } +func (*GetSBOMResponse) ProtoMessage() {} +func (*GetSBOMResponse) Descriptor() ([]byte, []int) { + return fileDescriptor_3d90829bc66d9c54, []int{13} +} +func (m *GetSBOMResponse) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetSBOMResponse.Unmarshal(m, b) +} +func (m *GetSBOMResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetSBOMResponse.Marshal(b, m, deterministic) +} +func (m *GetSBOMResponse) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetSBOMResponse.Merge(m, src) +} +func (m *GetSBOMResponse) XXX_Size() int { + return xxx_messageInfo_GetSBOMResponse.Size(m) +} +func (m *GetSBOMResponse) XXX_DiscardUnknown() { + xxx_messageInfo_GetSBOMResponse.DiscardUnknown(m) +} + +var xxx_messageInfo_GetSBOMResponse proto.InternalMessageInfo + +func (m *GetSBOMResponse) GetSuccess() bool { + if m != nil { + return m.Success + } + return false +} + +func (m *GetSBOMResponse) GetErrorMessage() string { + if m != nil { + return m.ErrorMessage + } + return "" +} + +func (m *GetSBOMResponse) GetErrorCode() ErrorCode { + if m != nil { + return m.ErrorCode + } + return ErrorCode_ERROR_CODE_UNSPECIFIED +} + +func (m *GetSBOMResponse) GetExists() bool { + if m != nil { + return m.Exists + } + return false +} + +func (m *GetSBOMResponse) GetMetadata() *SBOMMetadata { + if m != nil { + return m.Metadata + } + return nil +} + +func (m *GetSBOMResponse) GetSbom() *v1beta1.SBOMSyft { + if m != nil { + return m.Sbom + } + return nil +} + +// GetSBOMChunk is a single chunk of a streamed SBOM download. The first +// chunk sets metadata + status; subsequent chunks set only blob_chunk. +type GetSBOMChunk struct { + // metadata is set on the first chunk only. + Metadata *GetSBOMChunkMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // blob_chunk is a slice of the serialized SBOMSyft payload. + BlobChunk []byte `protobuf:"bytes,2,opt,name=blob_chunk,json=blobChunk,proto3" json:"blob_chunk,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetSBOMChunk) Reset() { *m = GetSBOMChunk{} } +func (m *GetSBOMChunk) String() string { return proto.CompactTextString(m) } +func (*GetSBOMChunk) ProtoMessage() {} +func (*GetSBOMChunk) Descriptor() ([]byte, []int) { + return fileDescriptor_3d90829bc66d9c54, []int{14} +} +func (m *GetSBOMChunk) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetSBOMChunk.Unmarshal(m, b) +} +func (m *GetSBOMChunk) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetSBOMChunk.Marshal(b, m, deterministic) +} +func (m *GetSBOMChunk) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetSBOMChunk.Merge(m, src) +} +func (m *GetSBOMChunk) XXX_Size() int { + return xxx_messageInfo_GetSBOMChunk.Size(m) +} +func (m *GetSBOMChunk) XXX_DiscardUnknown() { + xxx_messageInfo_GetSBOMChunk.DiscardUnknown(m) +} + +var xxx_messageInfo_GetSBOMChunk proto.InternalMessageInfo + +func (m *GetSBOMChunk) GetMetadata() *GetSBOMChunkMetadata { + if m != nil { + return m.Metadata + } + return nil +} + +func (m *GetSBOMChunk) GetBlobChunk() []byte { + if m != nil { + return m.BlobChunk + } + return nil +} + +// GetSBOMChunkMetadata is the status header sent on the first chunk of a +// GetSBOMStream call. If exists=false or success=false the server closes +// the stream without sending blob chunks. +type GetSBOMChunkMetadata struct { + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + ErrorMessage string `protobuf:"bytes,2,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + ErrorCode ErrorCode `protobuf:"varint,3,opt,name=error_code,json=errorCode,proto3,enum=storageserver.v1.ErrorCode" json:"error_code,omitempty"` + Exists bool `protobuf:"varint,4,opt,name=exists,proto3" json:"exists,omitempty"` + SbomMetadata *SBOMMetadata `protobuf:"bytes,5,opt,name=sbom_metadata,json=sbomMetadata,proto3" json:"sbom_metadata,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetSBOMChunkMetadata) Reset() { *m = GetSBOMChunkMetadata{} } +func (m *GetSBOMChunkMetadata) String() string { return proto.CompactTextString(m) } +func (*GetSBOMChunkMetadata) ProtoMessage() {} +func (*GetSBOMChunkMetadata) Descriptor() ([]byte, []int) { + return fileDescriptor_3d90829bc66d9c54, []int{15} +} +func (m *GetSBOMChunkMetadata) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetSBOMChunkMetadata.Unmarshal(m, b) +} +func (m *GetSBOMChunkMetadata) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetSBOMChunkMetadata.Marshal(b, m, deterministic) +} +func (m *GetSBOMChunkMetadata) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetSBOMChunkMetadata.Merge(m, src) +} +func (m *GetSBOMChunkMetadata) XXX_Size() int { + return xxx_messageInfo_GetSBOMChunkMetadata.Size(m) +} +func (m *GetSBOMChunkMetadata) XXX_DiscardUnknown() { + xxx_messageInfo_GetSBOMChunkMetadata.DiscardUnknown(m) +} + +var xxx_messageInfo_GetSBOMChunkMetadata proto.InternalMessageInfo + +func (m *GetSBOMChunkMetadata) GetSuccess() bool { + if m != nil { + return m.Success + } + return false +} + +func (m *GetSBOMChunkMetadata) GetErrorMessage() string { + if m != nil { + return m.ErrorMessage + } + return "" +} + +func (m *GetSBOMChunkMetadata) GetErrorCode() ErrorCode { + if m != nil { + return m.ErrorCode + } + return ErrorCode_ERROR_CODE_UNSPECIFIED +} + +func (m *GetSBOMChunkMetadata) GetExists() bool { + if m != nil { + return m.Exists + } + return false +} + +func (m *GetSBOMChunkMetadata) GetSbomMetadata() *SBOMMetadata { + if m != nil { + return m.SbomMetadata + } + return nil +} + +// SBOMMetadata is the indexed view of an SBOM row, returned by GetSBOM +// with or without metadata_only=true. +type SBOMMetadata struct { + // image_digest of the scanned image (without the "sha256:" prefix). + ImageDigest string `protobuf:"bytes,1,opt,name=image_digest,json=imageDigest,proto3" json:"image_digest,omitempty"` + // syft_version that produced the SBOM blob. + SyftVersion string `protobuf:"bytes,2,opt,name=syft_version,json=syftVersion,proto3" json:"syft_version,omitempty"` + // source identifies how this SBOM was produced. + Source SBOMSource `protobuf:"varint,3,opt,name=source,proto3,enum=storageserver.v1.SBOMSource" json:"source,omitempty"` + // blob_size_bytes is the size of the SBOM blob in S3. + BlobSizeBytes int64 `protobuf:"varint,4,opt,name=blob_size_bytes,json=blobSizeBytes,proto3" json:"blob_size_bytes,omitempty"` + // created_at is the RFC3339 timestamp when the row was first inserted. + CreatedAt string `protobuf:"bytes,5,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + // last_referenced_at is the RFC3339 timestamp when the orchestrator most + // recently resolved an inventory entry to this SBOM. Drives eviction. + LastReferencedAt string `protobuf:"bytes,6,opt,name=last_referenced_at,json=lastReferencedAt,proto3" json:"last_referenced_at,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *SBOMMetadata) Reset() { *m = SBOMMetadata{} } +func (m *SBOMMetadata) String() string { return proto.CompactTextString(m) } +func (*SBOMMetadata) ProtoMessage() {} +func (*SBOMMetadata) Descriptor() ([]byte, []int) { + return fileDescriptor_3d90829bc66d9c54, []int{16} +} +func (m *SBOMMetadata) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_SBOMMetadata.Unmarshal(m, b) +} +func (m *SBOMMetadata) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_SBOMMetadata.Marshal(b, m, deterministic) +} +func (m *SBOMMetadata) XXX_Merge(src proto.Message) { + xxx_messageInfo_SBOMMetadata.Merge(m, src) +} +func (m *SBOMMetadata) XXX_Size() int { + return xxx_messageInfo_SBOMMetadata.Size(m) +} +func (m *SBOMMetadata) XXX_DiscardUnknown() { + xxx_messageInfo_SBOMMetadata.DiscardUnknown(m) +} + +var xxx_messageInfo_SBOMMetadata proto.InternalMessageInfo + +func (m *SBOMMetadata) GetImageDigest() string { + if m != nil { + return m.ImageDigest + } + return "" +} + +func (m *SBOMMetadata) GetSyftVersion() string { + if m != nil { + return m.SyftVersion + } + return "" +} + +func (m *SBOMMetadata) GetSource() SBOMSource { + if m != nil { + return m.Source + } + return SBOMSource_SBOM_SOURCE_UNSPECIFIED +} + +func (m *SBOMMetadata) GetBlobSizeBytes() int64 { + if m != nil { + return m.BlobSizeBytes + } + return 0 +} + +func (m *SBOMMetadata) GetCreatedAt() string { + if m != nil { + return m.CreatedAt + } + return "" +} + +func (m *SBOMMetadata) GetLastReferencedAt() string { + if m != nil { + return m.LastReferencedAt + } + return "" +} + func init() { proto.RegisterEnum("storageserver.v1.ErrorCode", ErrorCode_name, ErrorCode_value) + proto.RegisterEnum("storageserver.v1.SBOMSource", SBOMSource_name, SBOMSource_value) proto.RegisterType((*SendContainerProfileRequest)(nil), "storageserver.v1.SendContainerProfileRequest") proto.RegisterType((*SendContainerProfileResponse)(nil), "storageserver.v1.SendContainerProfileResponse") proto.RegisterType((*GetProfileRequest)(nil), "storageserver.v1.GetProfileRequest") @@ -642,64 +1281,102 @@ func init() { proto.RegisterType((*ListApplicationProfilesResponse)(nil), "storageserver.v1.ListApplicationProfilesResponse") proto.RegisterType((*ListNetworkNeighborhoodsRequest)(nil), "storageserver.v1.ListNetworkNeighborhoodsRequest") proto.RegisterType((*ListNetworkNeighborhoodsResponse)(nil), "storageserver.v1.ListNetworkNeighborhoodsResponse") + proto.RegisterType((*PutSBOMRequest)(nil), "storageserver.v1.PutSBOMRequest") + proto.RegisterType((*PutSBOMResponse)(nil), "storageserver.v1.PutSBOMResponse") + proto.RegisterType((*PutSBOMChunk)(nil), "storageserver.v1.PutSBOMChunk") + proto.RegisterType((*PutSBOMChunkMetadata)(nil), "storageserver.v1.PutSBOMChunkMetadata") + proto.RegisterType((*GetSBOMRequest)(nil), "storageserver.v1.GetSBOMRequest") + proto.RegisterType((*GetSBOMResponse)(nil), "storageserver.v1.GetSBOMResponse") + proto.RegisterType((*GetSBOMChunk)(nil), "storageserver.v1.GetSBOMChunk") + proto.RegisterType((*GetSBOMChunkMetadata)(nil), "storageserver.v1.GetSBOMChunkMetadata") + proto.RegisterType((*SBOMMetadata)(nil), "storageserver.v1.SBOMMetadata") } func init() { proto.RegisterFile("storage_service.proto", fileDescriptor_3d90829bc66d9c54) } var fileDescriptor_3d90829bc66d9c54 = []byte{ - // 856 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x56, 0x41, 0x8f, 0xda, 0x46, - 0x14, 0xae, 0x81, 0x65, 0xcb, 0x4b, 0x1b, 0x39, 0xb3, 0x64, 0xe3, 0xb2, 0xdb, 0x14, 0x91, 0x1e, - 0x56, 0x95, 0x6a, 0x07, 0x7a, 0xa9, 0x7a, 0xa3, 0xe0, 0x4d, 0x91, 0x08, 0x26, 0x03, 0xa4, 0x52, - 0x2e, 0x96, 0xb1, 0xdf, 0xb2, 0x23, 0xc0, 0xe3, 0x7a, 0xcc, 0xe6, 0x56, 0xb5, 0x97, 0xa8, 0x55, - 0x7f, 0x41, 0x6f, 0xbd, 0xf5, 0xd0, 0x7f, 0xd0, 0x1c, 0xfa, 0x67, 0xfa, 0x43, 0x2a, 0xc6, 0x66, - 0x97, 0xc5, 0x06, 0x25, 0x52, 0xb6, 0xab, 0x9c, 0x98, 0xf9, 0xde, 0x9b, 0x37, 0x1f, 0xef, 0x7d, - 0xf3, 0xfc, 0xe0, 0xbe, 0x88, 0x78, 0xe8, 0x4c, 0xd0, 0x16, 0x18, 0x5e, 0x30, 0x17, 0xf5, 0x20, - 0xe4, 0x11, 0x27, 0x6a, 0x02, 0x2f, 0x51, 0x0c, 0xf5, 0x8b, 0x7a, 0xe5, 0xd9, 0x84, 0x45, 0xe7, - 0x8b, 0xb1, 0xee, 0xf2, 0xb9, 0x31, 0x5d, 0x8c, 0x51, 0xb8, 0x4e, 0x80, 0x46, 0xe2, 0x66, 0x04, - 0xd3, 0x89, 0xe1, 0x04, 0x4c, 0x18, 0x82, 0x9f, 0x45, 0x2f, 0x9d, 0x10, 0x5d, 0x3e, 0x0f, 0xb8, - 0x60, 0x11, 0xe3, 0xbe, 0x71, 0x51, 0x1f, 0x63, 0xe4, 0xd4, 0x8d, 0x09, 0xfa, 0x18, 0x3a, 0x11, - 0x7a, 0xf1, 0x25, 0xb5, 0x3f, 0x14, 0x38, 0x1a, 0xa0, 0xef, 0xb5, 0xb8, 0x1f, 0x39, 0xcc, 0xc7, - 0xb0, 0x1f, 0xf2, 0x33, 0x36, 0x43, 0x8a, 0x3f, 0x2c, 0x50, 0x44, 0xe4, 0x27, 0x05, 0xee, 0xb9, - 0x2b, 0x9b, 0x1d, 0xc4, 0x46, 0x4d, 0xa9, 0x2a, 0x27, 0x77, 0x1a, 0x03, 0xfd, 0x8a, 0x8f, 0x7e, - 0xc9, 0x47, 0x4f, 0xf8, 0xe8, 0xc1, 0x74, 0xa2, 0x2f, 0xf9, 0xe8, 0x19, 0x7c, 0xf4, 0x84, 0x8f, - 0x9e, 0xba, 0x57, 0x75, 0x37, 0x90, 0xda, 0xef, 0x0a, 0x1c, 0x67, 0x53, 0x14, 0x01, 0xf7, 0x05, - 0x12, 0x0d, 0xf6, 0xc5, 0xc2, 0x75, 0x51, 0x08, 0x49, 0xec, 0x43, 0xba, 0xda, 0x92, 0x47, 0xf0, - 0x31, 0x86, 0x21, 0x0f, 0xed, 0x39, 0x0a, 0xe1, 0x4c, 0x50, 0xcb, 0x55, 0x95, 0x93, 0x12, 0xfd, - 0x48, 0x82, 0x4f, 0x63, 0x8c, 0x7c, 0x03, 0x10, 0x3b, 0xb9, 0xdc, 0x43, 0x2d, 0x5f, 0x55, 0x4e, - 0xee, 0x36, 0x8e, 0xf4, 0xcd, 0xe4, 0xeb, 0xe6, 0xd2, 0xa7, 0xc5, 0x3d, 0xa4, 0x25, 0x5c, 0x2d, - 0x6b, 0x7f, 0x29, 0x70, 0xef, 0x09, 0x46, 0x1b, 0x49, 0x23, 0x50, 0x98, 0x32, 0xdf, 0x93, 0x6c, - 0x4a, 0x54, 0xae, 0xc9, 0x31, 0x94, 0x7c, 0x67, 0x8e, 0x22, 0x70, 0xdc, 0x15, 0x8d, 0x2b, 0x60, - 0x79, 0x62, 0xb9, 0x91, 0xb7, 0x97, 0xa8, 0x5c, 0x93, 0x43, 0x28, 0x86, 0x38, 0x61, 0xdc, 0xd7, - 0x0a, 0x12, 0x4d, 0x76, 0xe4, 0x6b, 0xd0, 0xdc, 0x19, 0x5f, 0x78, 0xb6, 0xe3, 0xba, 0x7c, 0xe1, - 0x47, 0x36, 0xf3, 0xd0, 0x8f, 0xd8, 0x19, 0xc3, 0x50, 0xdb, 0x93, 0x9e, 0x87, 0xd2, 0xde, 0x8c, - 0xcd, 0x9d, 0x4b, 0x6b, 0xed, 0xcf, 0x02, 0x90, 0x75, 0xb6, 0xb7, 0x9e, 0x3f, 0xf2, 0x4a, 0x81, - 0x03, 0x27, 0x08, 0x66, 0xcc, 0x75, 0x96, 0xb2, 0xb8, 0x14, 0x58, 0x41, 0x0a, 0x6c, 0xf4, 0x0e, - 0x04, 0xd6, 0xbc, 0x8a, 0xbe, 0xfa, 0xdf, 0xc4, 0x49, 0x61, 0xe4, 0x57, 0x05, 0xca, 0x3e, 0x46, - 0x2f, 0x79, 0x38, 0xb5, 0x7d, 0x64, 0x93, 0xf3, 0x31, 0x0f, 0xcf, 0x39, 0xf7, 0x64, 0x46, 0xef, - 0x34, 0x9e, 0xbf, 0x03, 0x26, 0xbd, 0x38, 0x7c, 0x6f, 0x2d, 0x3a, 0x3d, 0xf0, 0xd3, 0xe0, 0x96, - 0x37, 0x57, 0xfc, 0x3f, 0xdf, 0xdc, 0xdf, 0x0a, 0x3c, 0xec, 0x32, 0x11, 0xa5, 0xb3, 0x27, 0x56, - 0x22, 0xbf, 0x26, 0x68, 0x65, 0x53, 0xd0, 0x65, 0xd8, 0x9b, 0xb1, 0x39, 0x8b, 0x64, 0x25, 0xf3, - 0x34, 0xde, 0x2c, 0x65, 0xbe, 0xbc, 0x2a, 0x91, 0xa9, 0x5c, 0xaf, 0xc9, 0xbc, 0xf8, 0xc6, 0x32, - 0xdf, 0xdf, 0x29, 0xf3, 0xd7, 0x39, 0xf8, 0x6c, 0x2b, 0xf9, 0xdb, 0xd7, 0xfc, 0x2f, 0x0a, 0x94, - 0x33, 0x34, 0x2f, 0xb4, 0x42, 0x35, 0x7f, 0x73, 0xa2, 0x3f, 0x48, 0x8b, 0x5e, 0x64, 0xd5, 0xa3, - 0xf6, 0x5a, 0x89, 0xb3, 0x97, 0x21, 0xd7, 0xf7, 0xa0, 0xf6, 0xff, 0xe4, 0xa0, 0xba, 0x9d, 0xfd, - 0xed, 0x17, 0xff, 0x37, 0x05, 0xee, 0x67, 0xf5, 0x99, 0x55, 0xf5, 0x6f, 0xaa, 0xd1, 0x94, 0x33, - 0x1a, 0x4d, 0x66, 0xfd, 0xbf, 0x78, 0x95, 0x83, 0xd2, 0x25, 0x75, 0x52, 0x81, 0x43, 0x93, 0x52, - 0x8b, 0xda, 0x2d, 0xab, 0x6d, 0xda, 0xa3, 0xde, 0xa0, 0x6f, 0xb6, 0x3a, 0xa7, 0x1d, 0xb3, 0xad, - 0x7e, 0x40, 0x1e, 0x42, 0x65, 0xcd, 0xd6, 0xe9, 0x3d, 0x6f, 0x76, 0x3b, 0x6d, 0x9b, 0x9a, 0xcf, - 0x46, 0xe6, 0x60, 0xa8, 0x2a, 0xe4, 0x08, 0x1e, 0x5c, 0x3b, 0xdb, 0x1c, 0x0d, 0xbf, 0xb3, 0x68, - 0xe7, 0x85, 0xd9, 0x56, 0x73, 0xa4, 0x0a, 0xc7, 0x6b, 0xc6, 0x3e, 0xb5, 0x4e, 0x3b, 0x5d, 0xd3, - 0x1e, 0x5a, 0x96, 0xdd, 0x6d, 0xd2, 0x27, 0xa6, 0x9a, 0xdf, 0xe2, 0xd1, 0xb2, 0x9e, 0xf6, 0xbb, - 0xe6, 0xd0, 0x6c, 0xab, 0x85, 0x2d, 0x1e, 0x3d, 0x6b, 0x68, 0x9f, 0x5a, 0xa3, 0x5e, 0x5b, 0xdd, - 0x23, 0x9f, 0xc2, 0x27, 0xd7, 0x28, 0x0e, 0x4d, 0xda, 0x6b, 0x76, 0x6d, 0x89, 0xa9, 0xc5, 0x0d, - 0x86, 0xfd, 0x51, 0x77, 0xd0, 0xa4, 0x89, 0x71, 0xbf, 0xf1, 0x6f, 0x1e, 0xee, 0x0e, 0xe2, 0xbc, - 0x0f, 0xe2, 0xc1, 0x8c, 0x2c, 0xa0, 0x9c, 0x35, 0x89, 0x90, 0x2f, 0xd3, 0xd5, 0xdf, 0x31, 0x54, - 0x55, 0xf4, 0x37, 0x75, 0x4f, 0xf4, 0xfa, 0x3d, 0xc0, 0xd5, 0x67, 0x9b, 0x3c, 0x4a, 0x9f, 0x4e, - 0x8d, 0x20, 0x95, 0xcf, 0x77, 0x3b, 0x25, 0x81, 0x7f, 0x84, 0x07, 0x5b, 0x1a, 0x25, 0x79, 0x9c, - 0x0e, 0xb0, 0xfb, 0x83, 0x50, 0xa9, 0xbf, 0xc5, 0x89, 0xe4, 0xfe, 0x9f, 0x15, 0xd0, 0xb6, 0xbd, - 0x56, 0xb2, 0x25, 0xde, 0x8e, 0xbe, 0x54, 0x69, 0xbc, 0xcd, 0x91, 0x98, 0xc3, 0xb7, 0x8d, 0x17, - 0x8f, 0x33, 0xc7, 0xea, 0xb1, 0xe3, 0x4e, 0xd1, 0xf7, 0xe4, 0x58, 0xed, 0xce, 0x18, 0xfa, 0x91, - 0x71, 0x51, 0x37, 0xe4, 0xd4, 0x3c, 0x2e, 0xca, 0x9f, 0xaf, 0xfe, 0x0b, 0x00, 0x00, 0xff, 0xff, - 0x8b, 0xc1, 0x4a, 0xbf, 0xba, 0x0b, 0x00, 0x00, + // 1318 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x58, 0x41, 0x73, 0xdb, 0x44, + 0x14, 0x46, 0x8e, 0xeb, 0xc4, 0x2f, 0x4e, 0xaa, 0x6e, 0xdd, 0xd4, 0xa4, 0x69, 0x70, 0x5d, 0xa6, + 0x93, 0xe9, 0x80, 0xdc, 0x04, 0x0e, 0x4c, 0x6f, 0x8e, 0xad, 0xa6, 0x9e, 0x3a, 0x56, 0xba, 0xb2, + 0xdb, 0xa1, 0x17, 0x8d, 0x2c, 0x6f, 0x1c, 0x8d, 0x6d, 0xc9, 0xd5, 0xae, 0x53, 0x52, 0x66, 0x18, + 0xb8, 0x30, 0x30, 0x70, 0xe2, 0xc6, 0x8d, 0x1b, 0x07, 0x0e, 0xdc, 0xe9, 0x81, 0x61, 0x86, 0xbf, + 0x03, 0x7f, 0x01, 0x46, 0xab, 0x95, 0xa3, 0xd8, 0xb2, 0x69, 0x66, 0x1a, 0x0c, 0x27, 0x6b, 0xbf, + 0xf7, 0xf6, 0xbd, 0xcf, 0xef, 0x7d, 0x5a, 0x3d, 0x09, 0xae, 0x51, 0xe6, 0x7a, 0x66, 0x87, 0x18, + 0x94, 0x78, 0xc7, 0xb6, 0x45, 0x94, 0x81, 0xe7, 0x32, 0x17, 0xc9, 0x02, 0xf6, 0x51, 0xe2, 0x29, + 0xc7, 0xdb, 0xeb, 0x8f, 0x3b, 0x36, 0x3b, 0x1a, 0xb6, 0x14, 0xcb, 0xed, 0x17, 0xbb, 0xc3, 0x16, + 0xa1, 0x96, 0x39, 0x20, 0x45, 0xe1, 0x56, 0x1c, 0x74, 0x3b, 0x45, 0x73, 0x60, 0xd3, 0x22, 0x75, + 0x0f, 0xd9, 0x0b, 0xd3, 0x23, 0x96, 0xdb, 0x1f, 0xb8, 0xd4, 0x66, 0xb6, 0xeb, 0x14, 0x8f, 0xb7, + 0x5b, 0x84, 0x99, 0xdb, 0xc5, 0x0e, 0x71, 0x88, 0x67, 0x32, 0xd2, 0x0e, 0x92, 0x14, 0x7e, 0x90, + 0xe0, 0x86, 0x4e, 0x9c, 0x76, 0xd9, 0x75, 0x98, 0x69, 0x3b, 0xc4, 0x3b, 0xf0, 0xdc, 0x43, 0xbb, + 0x47, 0x30, 0x79, 0x3e, 0x24, 0x94, 0xa1, 0xcf, 0x25, 0xb8, 0x62, 0x85, 0x36, 0x63, 0x10, 0x18, + 0x73, 0x52, 0x5e, 0xda, 0x5a, 0xde, 0xd1, 0x95, 0x53, 0x3e, 0xca, 0x88, 0x8f, 0x22, 0xf8, 0x28, + 0x83, 0x6e, 0x47, 0xf1, 0xf9, 0x28, 0x31, 0x7c, 0x14, 0xc1, 0x47, 0x99, 0xc8, 0x2b, 0x5b, 0x63, + 0x48, 0xe1, 0x7b, 0x09, 0x36, 0xe2, 0x29, 0xd2, 0x81, 0xeb, 0x50, 0x82, 0x72, 0xb0, 0x48, 0x87, + 0x96, 0x45, 0x28, 0xe5, 0xc4, 0x96, 0x70, 0xb8, 0x44, 0xb7, 0x61, 0x85, 0x78, 0x9e, 0xeb, 0x19, + 0x7d, 0x42, 0xa9, 0xd9, 0x21, 0xb9, 0x44, 0x5e, 0xda, 0x4a, 0xe3, 0x0c, 0x07, 0xf7, 0x03, 0x0c, + 0xdd, 0x07, 0x08, 0x9c, 0x2c, 0xb7, 0x4d, 0x72, 0x0b, 0x79, 0x69, 0x6b, 0x75, 0xe7, 0x86, 0x32, + 0x5e, 0x7c, 0x45, 0xf5, 0x7d, 0xca, 0x6e, 0x9b, 0xe0, 0x34, 0x09, 0x2f, 0x0b, 0x3f, 0x49, 0x70, + 0x65, 0x8f, 0xb0, 0xb1, 0xa2, 0x21, 0x48, 0x76, 0x6d, 0xa7, 0xcd, 0xd9, 0xa4, 0x31, 0xbf, 0x46, + 0x1b, 0x90, 0x76, 0xcc, 0x3e, 0xa1, 0x03, 0xd3, 0x0a, 0x69, 0x9c, 0x02, 0xfe, 0x0e, 0x7f, 0xc1, + 0xb3, 0xa7, 0x31, 0xbf, 0x46, 0x6b, 0x90, 0xf2, 0x48, 0xc7, 0x76, 0x9d, 0x5c, 0x92, 0xa3, 0x62, + 0x85, 0x3e, 0x82, 0x9c, 0xd5, 0x73, 0x87, 0x6d, 0xc3, 0xb4, 0x2c, 0x77, 0xe8, 0x30, 0xc3, 0x6e, + 0x13, 0x87, 0xd9, 0x87, 0x36, 0xf1, 0x72, 0x97, 0xb8, 0xe7, 0x1a, 0xb7, 0x97, 0x02, 0x73, 0x75, + 0x64, 0x2d, 0xfc, 0x98, 0x04, 0x14, 0x65, 0x3b, 0xf7, 0xfa, 0xa1, 0x2f, 0x25, 0xb8, 0x6a, 0x0e, + 0x06, 0x3d, 0xdb, 0x32, 0x7d, 0x59, 0x8c, 0x04, 0x96, 0xe4, 0x02, 0x6b, 0xbe, 0x01, 0x81, 0x95, + 0x4e, 0xa3, 0x87, 0xff, 0x1b, 0x99, 0x13, 0x18, 0xfa, 0x5a, 0x82, 0xac, 0x43, 0xd8, 0x0b, 0xd7, + 0xeb, 0x1a, 0x0e, 0xb1, 0x3b, 0x47, 0x2d, 0xd7, 0x3b, 0x72, 0xdd, 0x36, 0xaf, 0xe8, 0xf2, 0xce, + 0x93, 0x37, 0xc0, 0xa4, 0x1e, 0x84, 0xaf, 0x47, 0xa2, 0xe3, 0xab, 0xce, 0x24, 0x38, 0xe5, 0x9e, + 0x4b, 0xfd, 0x9b, 0xf7, 0xdc, 0x2f, 0x12, 0x6c, 0xd6, 0x6c, 0xca, 0x26, 0xab, 0x47, 0x43, 0x91, + 0x9f, 0x11, 0xb4, 0x34, 0x2e, 0xe8, 0x2c, 0x5c, 0xea, 0xd9, 0x7d, 0x9b, 0xf1, 0x4e, 0x2e, 0xe0, + 0x60, 0xe1, 0xcb, 0xdc, 0x4f, 0x25, 0x64, 0xca, 0xaf, 0x23, 0x32, 0x4f, 0xbd, 0xb6, 0xcc, 0x17, + 0x67, 0xca, 0xfc, 0x55, 0x02, 0xde, 0x99, 0x4a, 0x7e, 0xfe, 0x9a, 0xff, 0x4a, 0x82, 0x6c, 0x8c, + 0xe6, 0x69, 0x2e, 0x99, 0x5f, 0xb8, 0x38, 0xd1, 0x5f, 0x9d, 0x14, 0x3d, 0x8d, 0xeb, 0x47, 0xe1, + 0x95, 0x14, 0x54, 0x2f, 0x46, 0xae, 0xff, 0x83, 0xde, 0xff, 0x9a, 0x80, 0xfc, 0x74, 0xf6, 0xf3, + 0x6f, 0xfe, 0x37, 0x12, 0x5c, 0x8b, 0x3b, 0x67, 0xc2, 0xee, 0x5f, 0xd4, 0x41, 0x93, 0x8d, 0x39, + 0x68, 0xe2, 0xfb, 0xff, 0x87, 0x04, 0xab, 0x07, 0x43, 0xa6, 0xef, 0x6a, 0xfb, 0x61, 0xbb, 0x6f, + 0x41, 0xc6, 0xee, 0xfb, 0x03, 0x4a, 0xdb, 0xee, 0x10, 0xca, 0x44, 0xc7, 0x97, 0x39, 0x56, 0xe1, + 0x90, 0xef, 0x42, 0x4f, 0x0e, 0x99, 0x71, 0x4c, 0x3c, 0xea, 0xf7, 0x33, 0xa8, 0xdb, 0xb2, 0x8f, + 0x3d, 0x09, 0x20, 0xf4, 0x21, 0xa4, 0xa8, 0x3b, 0xf4, 0xac, 0xb0, 0x64, 0x1b, 0x93, 0x25, 0xf3, + 0x93, 0xea, 0xdc, 0x07, 0x0b, 0x5f, 0x64, 0x40, 0x92, 0xb6, 0xdc, 0xbe, 0x78, 0x22, 0x3c, 0x7a, + 0x03, 0xe5, 0xe1, 0x49, 0x4e, 0x0e, 0x19, 0xe6, 0x81, 0x0b, 0xdf, 0x4a, 0x70, 0x79, 0xf4, 0x7f, + 0xe7, 0x3f, 0x51, 0x3c, 0x87, 0x8c, 0x60, 0x53, 0x3e, 0x1a, 0x3a, 0x5d, 0xb4, 0x0b, 0x4b, 0x7d, + 0xc2, 0xcc, 0xb6, 0xc9, 0x4c, 0x31, 0x76, 0xdd, 0x99, 0x8c, 0x14, 0xdd, 0xb1, 0x2f, 0xbc, 0xf1, + 0x68, 0x1f, 0xba, 0x09, 0xd0, 0xea, 0xb9, 0x2d, 0xc3, 0xf2, 0xed, 0x9c, 0x71, 0x06, 0xa7, 0x7d, + 0x84, 0x6f, 0x28, 0x7c, 0x27, 0x41, 0x36, 0x2e, 0xc2, 0x3c, 0xfb, 0x5e, 0xf8, 0x14, 0x56, 0xf7, + 0xc8, 0x05, 0xa8, 0xf0, 0x36, 0xac, 0x84, 0x75, 0x31, 0x5c, 0xa7, 0x77, 0xc2, 0x49, 0x2d, 0xe1, + 0x4c, 0x08, 0x6a, 0x4e, 0xef, 0xa4, 0xf0, 0x5b, 0x02, 0x2e, 0x8f, 0xb2, 0xcf, 0xff, 0xd0, 0x58, + 0x83, 0x14, 0xf9, 0xc4, 0xa6, 0x8c, 0xf2, 0xbb, 0x60, 0x09, 0x8b, 0x15, 0xba, 0x1f, 0xd1, 0x46, + 0x30, 0xa7, 0x6c, 0xc6, 0xd7, 0x36, 0x46, 0x13, 0xe1, 0x7d, 0x95, 0xba, 0xa8, 0xfb, 0xea, 0x39, + 0x64, 0x44, 0x09, 0xcf, 0x21, 0xe4, 0xe8, 0x8e, 0xf3, 0x0b, 0xf9, 0x4f, 0x09, 0xb2, 0x71, 0x11, + 0xfe, 0x8b, 0xbd, 0x2b, 0xc3, 0x8a, 0x5f, 0x26, 0xe3, 0x9c, 0x0d, 0xcc, 0xf8, 0x9b, 0xc2, 0x55, + 0xe1, 0x2f, 0x09, 0x32, 0x51, 0xf3, 0x5c, 0x4f, 0xea, 0x3b, 0x70, 0x99, 0x37, 0x87, 0xda, 0x2f, + 0x89, 0xd1, 0x3a, 0x61, 0x84, 0x8a, 0x01, 0x60, 0xc5, 0x87, 0x75, 0xfb, 0x25, 0xd9, 0xf5, 0x41, + 0xbf, 0x89, 0x96, 0x47, 0xfc, 0x77, 0x50, 0xc3, 0x0c, 0x1f, 0x3d, 0x69, 0x81, 0x94, 0x18, 0x7a, + 0x0f, 0x50, 0xcf, 0xa4, 0xcc, 0xf0, 0xc8, 0x21, 0xf1, 0x88, 0x63, 0x05, 0x6e, 0xc1, 0x7c, 0x20, + 0xfb, 0x16, 0x3c, 0x32, 0x94, 0xd8, 0xdd, 0xdf, 0x13, 0x90, 0x1e, 0xd5, 0x1d, 0xad, 0xc3, 0x9a, + 0x8a, 0xb1, 0x86, 0x8d, 0xb2, 0x56, 0x51, 0x8d, 0x66, 0x5d, 0x3f, 0x50, 0xcb, 0xd5, 0x07, 0x55, + 0xb5, 0x22, 0xbf, 0x85, 0x36, 0x61, 0x3d, 0x62, 0xab, 0xd6, 0x9f, 0x94, 0x6a, 0xd5, 0x8a, 0x81, + 0xd5, 0xc7, 0x4d, 0x55, 0x6f, 0xc8, 0x12, 0xba, 0x01, 0xd7, 0xcf, 0xec, 0x2d, 0x35, 0x1b, 0x0f, + 0x35, 0x5c, 0x7d, 0xa6, 0x56, 0xe4, 0x04, 0xca, 0xc3, 0x46, 0xc4, 0x78, 0x80, 0xb5, 0x07, 0xd5, + 0x9a, 0x6a, 0x34, 0x34, 0xcd, 0xa8, 0x95, 0xf0, 0x9e, 0x2a, 0x2f, 0x4c, 0xf1, 0x28, 0x6b, 0xfb, + 0x07, 0x35, 0xb5, 0xa1, 0x56, 0xe4, 0xe4, 0x14, 0x8f, 0xba, 0xd6, 0x30, 0x1e, 0x68, 0xcd, 0x7a, + 0x45, 0xbe, 0x84, 0x6e, 0xc2, 0xdb, 0x67, 0x28, 0x36, 0x54, 0x5c, 0x2f, 0xd5, 0x0c, 0x8e, 0xc9, + 0xa9, 0x31, 0x86, 0x07, 0xcd, 0x9a, 0x5e, 0xc2, 0xc2, 0xb8, 0x38, 0xb6, 0xd7, 0x6f, 0x4f, 0x24, + 0xf4, 0x52, 0x9c, 0xf9, 0x94, 0x7d, 0xfa, 0x2e, 0x05, 0x38, 0xed, 0xa8, 0x9f, 0x88, 0x7b, 0xe8, + 0x5a, 0x13, 0x97, 0xc7, 0xeb, 0x98, 0x83, 0x6c, 0xd4, 0xf8, 0x54, 0xc3, 0x8f, 0x6a, 0x5a, 0xa9, + 0x22, 0x4b, 0xe3, 0x16, 0xac, 0xee, 0x55, 0xf5, 0x06, 0xfe, 0x58, 0x4e, 0xa0, 0x2c, 0xc8, 0x51, + 0xcb, 0x43, 0x4d, 0x6f, 0xc8, 0x0b, 0x3b, 0x3f, 0xa7, 0x60, 0x55, 0x0f, 0x84, 0xa5, 0x07, 0x5f, + 0x3e, 0xd0, 0x10, 0xb2, 0x71, 0xaf, 0xfa, 0xe8, 0xfd, 0x18, 0x05, 0x4e, 0xff, 0x6a, 0xb1, 0xae, + 0xbc, 0xae, 0xbb, 0x38, 0xdb, 0x9f, 0x02, 0x9c, 0xbe, 0x17, 0xa3, 0xdb, 0xb1, 0xe7, 0xd2, 0x58, + 0x8a, 0x77, 0x67, 0x3b, 0x89, 0xc0, 0x9f, 0xc1, 0xf5, 0x29, 0x6f, 0x22, 0xe8, 0xde, 0x64, 0x80, + 0xd9, 0x6f, 0x5c, 0xeb, 0xdb, 0xe7, 0xd8, 0x21, 0xf2, 0x7f, 0x21, 0x41, 0x6e, 0xda, 0x38, 0x8c, + 0xa6, 0xc4, 0x9b, 0x31, 0xf8, 0xaf, 0xef, 0x9c, 0x67, 0x8b, 0xe0, 0x50, 0x87, 0x45, 0x31, 0x5d, + 0xa0, 0xfc, 0xd4, 0xd1, 0x25, 0x4c, 0x70, 0x6b, 0x86, 0x87, 0x88, 0xd7, 0x80, 0x15, 0x01, 0xe9, + 0xcc, 0x23, 0x66, 0x1f, 0x6d, 0xce, 0x1e, 0x88, 0x5e, 0x23, 0xe6, 0x96, 0xe4, 0xb3, 0x14, 0x8f, + 0x8e, 0x38, 0x96, 0x67, 0x47, 0x91, 0xb8, 0x88, 0xe3, 0xe3, 0x82, 0x0e, 0x2b, 0x02, 0x12, 0x2c, + 0xff, 0x39, 0xea, 0xe6, 0xec, 0xe7, 0xe1, 0x3d, 0x69, 0x77, 0xe7, 0xd9, 0xbd, 0xd8, 0x4f, 0x80, + 0x2d, 0xd3, 0xea, 0x12, 0xa7, 0xcd, 0x3f, 0x01, 0x5a, 0x3d, 0x9b, 0x38, 0xac, 0x78, 0xbc, 0x5d, + 0xe4, 0x5f, 0xf8, 0x5a, 0x29, 0xfe, 0xf3, 0xc1, 0xdf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x2f, 0x41, + 0xbc, 0x5a, 0x66, 0x14, 0x00, 0x00, } diff --git a/pkg/client/v1/proto/storage_service.proto b/pkg/client/v1/proto/storage_service.proto index e500d10..7473602 100644 --- a/pkg/client/v1/proto/storage_service.proto +++ b/pkg/client/v1/proto/storage_service.proto @@ -21,6 +21,31 @@ service StorageService { // ListNetworkNeighborhoods lists all NetworkNeighborhoods in a namespace (returns metadata only, nil Spec) rpc ListNetworkNeighborhoods(ListNetworkNeighborhoodsRequest) returns (ListNetworkNeighborhoodsResponse); + + // PutSBOM uploads an SBOM produced for an image. Idempotent on + // (customer_guid, image_digest, syft_version) — duplicate calls upsert the + // metadata row and overwrite the S3 blob; end-state is consistent. Used + // for SBOM payloads up to the default 4 MiB gRPC message limit. + rpc PutSBOM(PutSBOMRequest) returns (PutSBOMResponse); + + // PutSBOMStream uploads an SBOM larger than the default 4 MiB unary gRPC + // message limit, chunked client-side. The first chunk in the stream MUST + // set metadata (image_digest, syft_version, source); subsequent chunks + // set only blob_chunk. Concatenated chunks form the serialized SBOMSyft + // proto payload. + rpc PutSBOMStream(stream PutSBOMChunk) returns (PutSBOMResponse); + + // GetSBOM probes for or fetches an SBOM by (customer_guid, image_digest, + // syft_version). Set metadata_only=true to skip transferring the blob — + // used by agents to decide whether to regenerate. Returns the same + // envelope (with exists=false) on a miss. + rpc GetSBOM(GetSBOMRequest) returns (GetSBOMResponse); + + // GetSBOMStream returns an SBOM larger than the default 4 MiB unary gRPC + // message limit, server-streamed. The first chunk sets metadata + status; + // subsequent chunks set only blob_chunk. The metadata_only flag on the + // request is ignored — GetSBOM (unary) is the metadata-only entry point. + rpc GetSBOMStream(GetSBOMRequest) returns (stream GetSBOMChunk); } // SendContainerProfileRequest contains the container profile to be stored @@ -92,6 +117,19 @@ enum ErrorCode { ERROR_CODE_PROFILE_NOT_FOUND = 5; ERROR_CODE_INTERNAL_ERROR = 6; ERROR_CODE_PULSAR_ERROR = 7; + ERROR_CODE_SBOM_NOT_FOUND = 8; + ERROR_CODE_SBOM_TOO_LARGE = 9; +} + +// SBOMSource identifies how an SBOM entered the backend. +enum SBOMSource { + SBOM_SOURCE_UNSPECIFIED = 0; + // SBOM_SOURCE_WORKLOAD: generated by node-agent for an in-cluster workload image. + SBOM_SOURCE_WORKLOAD = 1; + // SBOM_SOURCE_REGISTRY: generated by kubevuln for a registry image (Registry Epic). + SBOM_SOURCE_REGISTRY = 2; + // SBOM_SOURCE_HOST: generated by the host scanner for an EC2 host snapshot. + SBOM_SOURCE_HOST = 3; } // ListApplicationProfilesRequest requests a list of ApplicationProfiles in a namespace @@ -167,3 +205,134 @@ message ListNetworkNeighborhoodsResponse { // Continue token for next page (empty if no more results) string cont = 5; } + +// PutSBOMRequest uploads an SBOM blob plus the keys identifying it. +// customer_guid, cluster, host_type, and host_id are sent via gRPC metadata +// headers (see the AuthInterceptor on the server side). The unary path is +// intended for SBOMs up to ~4 MiB; use PutSBOMStream for larger payloads. +message PutSBOMRequest { + // image_digest is the SHA-256 digest of the scanned image (without the + // "sha256:" prefix). Part of the primary key. + string image_digest = 1; + + // syft_version is the version of syft that produced the blob. Part of the + // primary key — multiple syft versions can coexist for one + // (customer_guid, image_digest) during gradual fleet upgrades. + string syft_version = 2; + + // source identifies how this SBOM was produced. + SBOMSource source = 3; + + // sbom is the SBOM payload as a SBOMSyft K8s resource (the same shape + // produced by syft + v1beta1.StripSBOM in node-agent / kubevuln). + github.com.kubescape.storage.pkg.apis.softwarecomposition.v1beta1.SBOMSyft sbom = 4; +} + +// PutSBOMResponse indicates success or failure of the operation. S3 + +// Postgres persistence happens asynchronously via Pulsar; success here +// means the upload was accepted and the Pulsar message was published. +message PutSBOMResponse { + bool success = 1; + string error_message = 2; + ErrorCode error_code = 3; +} + +// PutSBOMChunk is a single chunk of a streamed SBOM upload. The first +// chunk in the stream MUST set metadata. Subsequent chunks set only +// blob_chunk. Concatenated blob_chunk values form the serialized SBOMSyft +// proto payload. +message PutSBOMChunk { + // metadata is set on the first chunk only. + PutSBOMChunkMetadata metadata = 1; + + // blob_chunk is a slice of the serialized SBOMSyft payload. + bytes blob_chunk = 2; +} + +// PutSBOMChunkMetadata is the metadata header sent on the first chunk of a +// PutSBOMStream call. +message PutSBOMChunkMetadata { + string image_digest = 1; + string syft_version = 2; + SBOMSource source = 3; +} + +// GetSBOMRequest probes for or fetches an SBOM by primary key. +// customer_guid is sent via gRPC metadata headers. +message GetSBOMRequest { + // image_digest is the SHA-256 digest of the requested image (without the + // "sha256:" prefix). + string image_digest = 1; + + // syft_version is the syft version that produced the requested SBOM. The + // (image_digest, syft_version) pair is the lookup key. Leave empty to + // request "latest available version" semantics; the server returns the + // most recently created SBOM for the image. + string syft_version = 2; + + // metadata_only skips the blob and returns only SBOMMetadata. Used by + // agents to decide whether to regenerate. + bool metadata_only = 3; +} + +// GetSBOMResponse returns the SBOM (or just metadata). +message GetSBOMResponse { + bool success = 1; + string error_message = 2; + ErrorCode error_code = 3; + + // exists indicates whether an SBOM row was found for the requested key. + // When false, success is still true and metadata + sbom are empty. + bool exists = 4; + + // metadata is populated whenever exists=true. + SBOMMetadata metadata = 5; + + // sbom is populated only when exists=true and metadata_only was false on + // the request. + github.com.kubescape.storage.pkg.apis.softwarecomposition.v1beta1.SBOMSyft sbom = 6; +} + +// GetSBOMChunk is a single chunk of a streamed SBOM download. The first +// chunk sets metadata + status; subsequent chunks set only blob_chunk. +message GetSBOMChunk { + // metadata is set on the first chunk only. + GetSBOMChunkMetadata metadata = 1; + + // blob_chunk is a slice of the serialized SBOMSyft payload. + bytes blob_chunk = 2; +} + +// GetSBOMChunkMetadata is the status header sent on the first chunk of a +// GetSBOMStream call. If exists=false or success=false the server closes +// the stream without sending blob chunks. +message GetSBOMChunkMetadata { + bool success = 1; + string error_message = 2; + ErrorCode error_code = 3; + bool exists = 4; + SBOMMetadata sbom_metadata = 5; +} + +// SBOMMetadata is the indexed view of an SBOM row, returned by GetSBOM +// with or without metadata_only=true. +message SBOMMetadata { + // image_digest of the scanned image (without the "sha256:" prefix). + string image_digest = 1; + + // syft_version that produced the SBOM blob. + string syft_version = 2; + + // source identifies how this SBOM was produced. + SBOMSource source = 3; + + // blob_size_bytes is the size of the SBOM blob in S3. + int64 blob_size_bytes = 4; + + // created_at is the RFC3339 timestamp when the row was first inserted. + string created_at = 5; + + // last_referenced_at is the RFC3339 timestamp when the orchestrator most + // recently resolved an inventory entry to this SBOM. Drives eviction. + string last_referenced_at = 6; +} diff --git a/pkg/client/v1/proto/storage_service_grpc.pb.go b/pkg/client/v1/proto/storage_service_grpc.pb.go index 03c98e4..e3f1ac0 100644 --- a/pkg/client/v1/proto/storage_service_grpc.pb.go +++ b/pkg/client/v1/proto/storage_service_grpc.pb.go @@ -23,6 +23,10 @@ const ( StorageService_GetProfile_FullMethodName = "/storageserver.v1.StorageService/GetProfile" StorageService_ListApplicationProfiles_FullMethodName = "/storageserver.v1.StorageService/ListApplicationProfiles" StorageService_ListNetworkNeighborhoods_FullMethodName = "/storageserver.v1.StorageService/ListNetworkNeighborhoods" + StorageService_PutSBOM_FullMethodName = "/storageserver.v1.StorageService/PutSBOM" + StorageService_PutSBOMStream_FullMethodName = "/storageserver.v1.StorageService/PutSBOMStream" + StorageService_GetSBOM_FullMethodName = "/storageserver.v1.StorageService/GetSBOM" + StorageService_GetSBOMStream_FullMethodName = "/storageserver.v1.StorageService/GetSBOMStream" ) // StorageServiceClient is the client API for StorageService service. @@ -34,13 +38,34 @@ type StorageServiceClient interface { // SendContainerProfile receives a container profile (time-series snapshot) from node agent // and sends it to Pulsar for processing by the ingester SendContainerProfile(ctx context.Context, in *SendContainerProfileRequest, opts ...grpc.CallOption) (*SendContainerProfileResponse, error) - // GetProfile retrieves an aggregated profile (ApplicationProfile or NetworkNeighborhood) - // by fetching container profiles from S3 and aggregating them + // GetProfile retrieves an aggregated profile (ApplicationProfile, NetworkNeighborhood, + // or ContainerProfile) by fetching them from S3 GetProfile(ctx context.Context, in *GetProfileRequest, opts ...grpc.CallOption) (*GetProfileResponse, error) // ListApplicationProfiles lists all ApplicationProfiles in a namespace (returns metadata only, nil Spec) ListApplicationProfiles(ctx context.Context, in *ListApplicationProfilesRequest, opts ...grpc.CallOption) (*ListApplicationProfilesResponse, error) // ListNetworkNeighborhoods lists all NetworkNeighborhoods in a namespace (returns metadata only, nil Spec) ListNetworkNeighborhoods(ctx context.Context, in *ListNetworkNeighborhoodsRequest, opts ...grpc.CallOption) (*ListNetworkNeighborhoodsResponse, error) + // PutSBOM uploads an SBOM produced for an image. Idempotent on + // (customer_guid, image_digest, syft_version) — duplicate calls upsert the + // metadata row and overwrite the S3 blob; end-state is consistent. Used + // for SBOM payloads up to the default 4 MiB gRPC message limit. + PutSBOM(ctx context.Context, in *PutSBOMRequest, opts ...grpc.CallOption) (*PutSBOMResponse, error) + // PutSBOMStream uploads an SBOM larger than the default 4 MiB unary gRPC + // message limit, chunked client-side. The first chunk in the stream MUST + // set metadata (image_digest, syft_version, source); subsequent chunks + // set only blob_chunk. Concatenated chunks form the serialized SBOMSyft + // proto payload. + PutSBOMStream(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[PutSBOMChunk, PutSBOMResponse], error) + // GetSBOM probes for or fetches an SBOM by (customer_guid, image_digest, + // syft_version). Set metadata_only=true to skip transferring the blob — + // used by agents to decide whether to regenerate. Returns the same + // envelope (with exists=false) on a miss. + GetSBOM(ctx context.Context, in *GetSBOMRequest, opts ...grpc.CallOption) (*GetSBOMResponse, error) + // GetSBOMStream returns an SBOM larger than the default 4 MiB unary gRPC + // message limit, server-streamed. The first chunk sets metadata + status; + // subsequent chunks set only blob_chunk. The metadata_only flag on the + // request is ignored — GetSBOM (unary) is the metadata-only entry point. + GetSBOMStream(ctx context.Context, in *GetSBOMRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GetSBOMChunk], error) } type storageServiceClient struct { @@ -91,6 +116,58 @@ func (c *storageServiceClient) ListNetworkNeighborhoods(ctx context.Context, in return out, nil } +func (c *storageServiceClient) PutSBOM(ctx context.Context, in *PutSBOMRequest, opts ...grpc.CallOption) (*PutSBOMResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(PutSBOMResponse) + err := c.cc.Invoke(ctx, StorageService_PutSBOM_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *storageServiceClient) PutSBOMStream(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[PutSBOMChunk, PutSBOMResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StorageService_ServiceDesc.Streams[0], StorageService_PutSBOMStream_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[PutSBOMChunk, PutSBOMResponse]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StorageService_PutSBOMStreamClient = grpc.ClientStreamingClient[PutSBOMChunk, PutSBOMResponse] + +func (c *storageServiceClient) GetSBOM(ctx context.Context, in *GetSBOMRequest, opts ...grpc.CallOption) (*GetSBOMResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetSBOMResponse) + err := c.cc.Invoke(ctx, StorageService_GetSBOM_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *storageServiceClient) GetSBOMStream(ctx context.Context, in *GetSBOMRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GetSBOMChunk], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StorageService_ServiceDesc.Streams[1], StorageService_GetSBOMStream_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[GetSBOMRequest, GetSBOMChunk]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StorageService_GetSBOMStreamClient = grpc.ServerStreamingClient[GetSBOMChunk] + // StorageServiceServer is the server API for StorageService service. // All implementations must embed UnimplementedStorageServiceServer // for forward compatibility. @@ -100,13 +177,34 @@ type StorageServiceServer interface { // SendContainerProfile receives a container profile (time-series snapshot) from node agent // and sends it to Pulsar for processing by the ingester SendContainerProfile(context.Context, *SendContainerProfileRequest) (*SendContainerProfileResponse, error) - // GetProfile retrieves an aggregated profile (ApplicationProfile or NetworkNeighborhood) - // by fetching container profiles from S3 and aggregating them + // GetProfile retrieves an aggregated profile (ApplicationProfile, NetworkNeighborhood, + // or ContainerProfile) by fetching them from S3 GetProfile(context.Context, *GetProfileRequest) (*GetProfileResponse, error) // ListApplicationProfiles lists all ApplicationProfiles in a namespace (returns metadata only, nil Spec) ListApplicationProfiles(context.Context, *ListApplicationProfilesRequest) (*ListApplicationProfilesResponse, error) // ListNetworkNeighborhoods lists all NetworkNeighborhoods in a namespace (returns metadata only, nil Spec) ListNetworkNeighborhoods(context.Context, *ListNetworkNeighborhoodsRequest) (*ListNetworkNeighborhoodsResponse, error) + // PutSBOM uploads an SBOM produced for an image. Idempotent on + // (customer_guid, image_digest, syft_version) — duplicate calls upsert the + // metadata row and overwrite the S3 blob; end-state is consistent. Used + // for SBOM payloads up to the default 4 MiB gRPC message limit. + PutSBOM(context.Context, *PutSBOMRequest) (*PutSBOMResponse, error) + // PutSBOMStream uploads an SBOM larger than the default 4 MiB unary gRPC + // message limit, chunked client-side. The first chunk in the stream MUST + // set metadata (image_digest, syft_version, source); subsequent chunks + // set only blob_chunk. Concatenated chunks form the serialized SBOMSyft + // proto payload. + PutSBOMStream(grpc.ClientStreamingServer[PutSBOMChunk, PutSBOMResponse]) error + // GetSBOM probes for or fetches an SBOM by (customer_guid, image_digest, + // syft_version). Set metadata_only=true to skip transferring the blob — + // used by agents to decide whether to regenerate. Returns the same + // envelope (with exists=false) on a miss. + GetSBOM(context.Context, *GetSBOMRequest) (*GetSBOMResponse, error) + // GetSBOMStream returns an SBOM larger than the default 4 MiB unary gRPC + // message limit, server-streamed. The first chunk sets metadata + status; + // subsequent chunks set only blob_chunk. The metadata_only flag on the + // request is ignored — GetSBOM (unary) is the metadata-only entry point. + GetSBOMStream(*GetSBOMRequest, grpc.ServerStreamingServer[GetSBOMChunk]) error mustEmbedUnimplementedStorageServiceServer() } @@ -129,6 +227,18 @@ func (UnimplementedStorageServiceServer) ListApplicationProfiles(context.Context func (UnimplementedStorageServiceServer) ListNetworkNeighborhoods(context.Context, *ListNetworkNeighborhoodsRequest) (*ListNetworkNeighborhoodsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListNetworkNeighborhoods not implemented") } +func (UnimplementedStorageServiceServer) PutSBOM(context.Context, *PutSBOMRequest) (*PutSBOMResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method PutSBOM not implemented") +} +func (UnimplementedStorageServiceServer) PutSBOMStream(grpc.ClientStreamingServer[PutSBOMChunk, PutSBOMResponse]) error { + return status.Errorf(codes.Unimplemented, "method PutSBOMStream not implemented") +} +func (UnimplementedStorageServiceServer) GetSBOM(context.Context, *GetSBOMRequest) (*GetSBOMResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetSBOM not implemented") +} +func (UnimplementedStorageServiceServer) GetSBOMStream(*GetSBOMRequest, grpc.ServerStreamingServer[GetSBOMChunk]) error { + return status.Errorf(codes.Unimplemented, "method GetSBOMStream not implemented") +} func (UnimplementedStorageServiceServer) mustEmbedUnimplementedStorageServiceServer() {} func (UnimplementedStorageServiceServer) testEmbeddedByValue() {} @@ -222,6 +332,60 @@ func _StorageService_ListNetworkNeighborhoods_Handler(srv interface{}, ctx conte return interceptor(ctx, in, info, handler) } +func _StorageService_PutSBOM_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(PutSBOMRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StorageServiceServer).PutSBOM(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StorageService_PutSBOM_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StorageServiceServer).PutSBOM(ctx, req.(*PutSBOMRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StorageService_PutSBOMStream_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(StorageServiceServer).PutSBOMStream(&grpc.GenericServerStream[PutSBOMChunk, PutSBOMResponse]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StorageService_PutSBOMStreamServer = grpc.ClientStreamingServer[PutSBOMChunk, PutSBOMResponse] + +func _StorageService_GetSBOM_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetSBOMRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(StorageServiceServer).GetSBOM(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: StorageService_GetSBOM_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(StorageServiceServer).GetSBOM(ctx, req.(*GetSBOMRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _StorageService_GetSBOMStream_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(GetSBOMRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StorageServiceServer).GetSBOMStream(m, &grpc.GenericServerStream[GetSBOMRequest, GetSBOMChunk]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StorageService_GetSBOMStreamServer = grpc.ServerStreamingServer[GetSBOMChunk] + // StorageService_ServiceDesc is the grpc.ServiceDesc for StorageService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -245,7 +409,26 @@ var StorageService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListNetworkNeighborhoods", Handler: _StorageService_ListNetworkNeighborhoods_Handler, }, + { + MethodName: "PutSBOM", + Handler: _StorageService_PutSBOM_Handler, + }, + { + MethodName: "GetSBOM", + Handler: _StorageService_GetSBOM_Handler, + }, + }, + Streams: []grpc.StreamDesc{ + { + StreamName: "PutSBOMStream", + Handler: _StorageService_PutSBOMStream_Handler, + ClientStreams: true, + }, + { + StreamName: "GetSBOMStream", + Handler: _StorageService_GetSBOMStream_Handler, + ServerStreams: true, + }, }, - Streams: []grpc.StreamDesc{}, Metadata: "storage_service.proto", } diff --git a/pkg/client/v1/storageclient.go b/pkg/client/v1/storageclient.go index ef3c38f..08a714a 100644 --- a/pkg/client/v1/storageclient.go +++ b/pkg/client/v1/storageclient.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "fmt" + "io" "net/url" "strconv" "strings" @@ -17,6 +18,11 @@ import ( "google.golang.org/grpc/metadata" ) +// sbomStreamChunkSize is the per-chunk byte budget used by PutSBOMStream +// and GetSBOMStream. Set well below the default 4 MiB gRPC message limit +// to leave headroom for framing overhead. +const sbomStreamChunkSize = 1 << 20 // 1 MiB + // Default gRPC ports const ( DefaultGRPCPort = 50051 // Non-secure gRPC @@ -486,3 +492,186 @@ func (c *StorageClient) ListNetworkNeighborhoods(ctx context.Context, namespace return list, nil } + +// PutSBOM uploads an SBOM to the storage server. Use for payloads small +// enough to fit in a single gRPC message (~4 MiB). For larger SBOMs, use +// PutSBOMStream instead. +func (c *StorageClient) PutSBOM(ctx context.Context, imageDigest, syftVersion string, source proto.SBOMSource, sbom *v1beta1.SBOMSyft) (*proto.PutSBOMResponse, error) { + if c.protoClient == nil { + return nil, fmt.Errorf("client is not connected") + } + + req := &proto.PutSBOMRequest{ + ImageDigest: imageDigest, + SyftVersion: syftVersion, + Source: source, + Sbom: sbom, + } + + ctx = c.withMetadata(ctx) + + if c.callTimeout != nil && *c.callTimeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *c.callTimeout) + defer cancel() + } + + return c.protoClient.PutSBOM(ctx, req) +} + +// PutSBOMStream uploads an SBOM to the storage server in chunks. The SBOM +// is marshaled to its proto wire form and split into ~1 MiB chunks; the +// first chunk carries metadata, subsequent chunks carry blob bytes only. +// Use when the SBOM is too large for unary PutSBOM (~4 MiB default gRPC +// message limit). +func (c *StorageClient) PutSBOMStream(ctx context.Context, imageDigest, syftVersion string, source proto.SBOMSource, sbom *v1beta1.SBOMSyft) (*proto.PutSBOMResponse, error) { + if c.protoClient == nil { + return nil, fmt.Errorf("client is not connected") + } + + if sbom == nil { + return nil, fmt.Errorf("sbom is nil") + } + + payload, err := sbom.Marshal() + if err != nil { + return nil, fmt.Errorf("failed to marshal SBOMSyft: %w", err) + } + + ctx = c.withMetadata(ctx) + + if c.callTimeout != nil && *c.callTimeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *c.callTimeout) + defer cancel() + } + + stream, err := c.protoClient.PutSBOMStream(ctx) + if err != nil { + return nil, fmt.Errorf("failed to open PutSBOMStream: %w", err) + } + + // First chunk carries metadata + (optionally) the first slice of bytes. + first := &proto.PutSBOMChunk{ + Metadata: &proto.PutSBOMChunkMetadata{ + ImageDigest: imageDigest, + SyftVersion: syftVersion, + Source: source, + }, + } + cut := sbomStreamChunkSize + if cut > len(payload) { + cut = len(payload) + } + first.BlobChunk = payload[:cut] + if err := stream.Send(first); err != nil { + return nil, fmt.Errorf("failed to send first chunk: %w", err) + } + + // Subsequent chunks carry blob bytes only. + for offset := cut; offset < len(payload); offset += sbomStreamChunkSize { + end := offset + sbomStreamChunkSize + if end > len(payload) { + end = len(payload) + } + if err := stream.Send(&proto.PutSBOMChunk{BlobChunk: payload[offset:end]}); err != nil { + return nil, fmt.Errorf("failed to send chunk: %w", err) + } + } + + return stream.CloseAndRecv() +} + +// GetSBOM probes for or fetches an SBOM by (image_digest, syft_version). +// Set metadataOnly=true to skip the blob and only check whether an SBOM +// exists. For SBOMs too large to fit in a single gRPC message, use +// GetSBOMStream when metadataOnly is false. +func (c *StorageClient) GetSBOM(ctx context.Context, imageDigest, syftVersion string, metadataOnly bool) (*proto.GetSBOMResponse, error) { + if c.protoClient == nil { + return nil, fmt.Errorf("client is not connected") + } + + req := &proto.GetSBOMRequest{ + ImageDigest: imageDigest, + SyftVersion: syftVersion, + MetadataOnly: metadataOnly, + } + + ctx = c.withMetadata(ctx) + + if c.callTimeout != nil && *c.callTimeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *c.callTimeout) + defer cancel() + } + + return c.protoClient.GetSBOM(ctx, req) +} + +// GetSBOMStream fetches an SBOM in chunks. The first stream chunk returns +// metadata + status; subsequent chunks carry the marshaled SBOMSyft bytes, +// which are concatenated and unmarshaled by this method. Returns +// (metadata, nil) when the server reports exists=false or success=false — +// the caller should consult metadata.Success / metadata.Exists before +// using the returned SBOMSyft. +func (c *StorageClient) GetSBOMStream(ctx context.Context, imageDigest, syftVersion string) (*proto.GetSBOMChunkMetadata, *v1beta1.SBOMSyft, error) { + if c.protoClient == nil { + return nil, nil, fmt.Errorf("client is not connected") + } + + req := &proto.GetSBOMRequest{ + ImageDigest: imageDigest, + SyftVersion: syftVersion, + } + + ctx = c.withMetadata(ctx) + + if c.callTimeout != nil && *c.callTimeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *c.callTimeout) + defer cancel() + } + + stream, err := c.protoClient.GetSBOMStream(ctx, req) + if err != nil { + return nil, nil, fmt.Errorf("failed to open GetSBOMStream: %w", err) + } + + // First chunk MUST carry metadata. + firstChunk, err := stream.Recv() + if err != nil { + return nil, nil, fmt.Errorf("failed to receive first chunk: %w", err) + } + md := firstChunk.GetMetadata() + if md == nil { + return nil, nil, fmt.Errorf("first GetSBOMStream chunk missing metadata") + } + + // On error or miss the server closes the stream after the metadata chunk. + if !md.Success || !md.Exists { + return md, nil, nil + } + + // Accumulate blob bytes. The first chunk may have carried some payload + // alongside the metadata. + var buf []byte + if len(firstChunk.BlobChunk) > 0 { + buf = append(buf, firstChunk.BlobChunk...) + } + for { + chunk, err := stream.Recv() + if err == io.EOF { + break + } + if err != nil { + return md, nil, fmt.Errorf("failed to receive chunk: %w", err) + } + buf = append(buf, chunk.BlobChunk...) + } + + sbom := &v1beta1.SBOMSyft{} + if err := sbom.Unmarshal(buf); err != nil { + return md, nil, fmt.Errorf("failed to unmarshal SBOMSyft: %w", err) + } + return md, sbom, nil +} diff --git a/pkg/client/v1/storageclient_test.go b/pkg/client/v1/storageclient_test.go index 0952909..26a5ea2 100644 --- a/pkg/client/v1/storageclient_test.go +++ b/pkg/client/v1/storageclient_test.go @@ -2,6 +2,7 @@ package v1 import ( "context" + "fmt" "testing" "time" @@ -19,6 +20,10 @@ type mockStorageServiceClient struct { getProfileFunc func(ctx context.Context, in *proto.GetProfileRequest, opts ...grpc.CallOption) (*proto.GetProfileResponse, error) listApplicationProfilesFunc func(ctx context.Context, in *proto.ListApplicationProfilesRequest, opts ...grpc.CallOption) (*proto.ListApplicationProfilesResponse, error) listNetworkNeighborhoodsFunc func(ctx context.Context, in *proto.ListNetworkNeighborhoodsRequest, opts ...grpc.CallOption) (*proto.ListNetworkNeighborhoodsResponse, error) + putSBOMFunc func(ctx context.Context, in *proto.PutSBOMRequest, opts ...grpc.CallOption) (*proto.PutSBOMResponse, error) + getSBOMFunc func(ctx context.Context, in *proto.GetSBOMRequest, opts ...grpc.CallOption) (*proto.GetSBOMResponse, error) + putSBOMStreamFunc func(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[proto.PutSBOMChunk, proto.PutSBOMResponse], error) + getSBOMStreamFunc func(ctx context.Context, in *proto.GetSBOMRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[proto.GetSBOMChunk], error) } func (m *mockStorageServiceClient) SendContainerProfile(ctx context.Context, in *proto.SendContainerProfileRequest, opts ...grpc.CallOption) (*proto.SendContainerProfileResponse, error) { @@ -49,6 +54,34 @@ func (m *mockStorageServiceClient) ListNetworkNeighborhoods(ctx context.Context, return &proto.ListNetworkNeighborhoodsResponse{Success: true}, nil } +func (m *mockStorageServiceClient) PutSBOM(ctx context.Context, in *proto.PutSBOMRequest, opts ...grpc.CallOption) (*proto.PutSBOMResponse, error) { + if m.putSBOMFunc != nil { + return m.putSBOMFunc(ctx, in, opts...) + } + return &proto.PutSBOMResponse{Success: true}, nil +} + +func (m *mockStorageServiceClient) GetSBOM(ctx context.Context, in *proto.GetSBOMRequest, opts ...grpc.CallOption) (*proto.GetSBOMResponse, error) { + if m.getSBOMFunc != nil { + return m.getSBOMFunc(ctx, in, opts...) + } + return &proto.GetSBOMResponse{Success: true}, nil +} + +func (m *mockStorageServiceClient) PutSBOMStream(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[proto.PutSBOMChunk, proto.PutSBOMResponse], error) { + if m.putSBOMStreamFunc != nil { + return m.putSBOMStreamFunc(ctx, opts...) + } + return nil, fmt.Errorf("PutSBOMStream not implemented in mock") +} + +func (m *mockStorageServiceClient) GetSBOMStream(ctx context.Context, in *proto.GetSBOMRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[proto.GetSBOMChunk], error) { + if m.getSBOMStreamFunc != nil { + return m.getSBOMStreamFunc(ctx, in, opts...) + } + return nil, fmt.Errorf("GetSBOMStream not implemented in mock") +} + func TestNewStorageClient(t *testing.T) { tests := []struct { name string @@ -615,3 +648,100 @@ func TestParseGRPCURL(t *testing.T) { }) } } + +func TestStorageClient_PutSBOM(t *testing.T) { + client, err := NewStorageClient("grpc://storage.example.com:50051", "test-account", "test-key", "test-cluster") + require.NoError(t, err) + + const ( + imageDigest = "abc123def456" + syftVersion = "1.0.0" + ) + + mockClient := &mockStorageServiceClient{ + putSBOMFunc: func(ctx context.Context, in *proto.PutSBOMRequest, opts ...grpc.CallOption) (*proto.PutSBOMResponse, error) { + assert.Equal(t, imageDigest, in.ImageDigest) + assert.Equal(t, syftVersion, in.SyftVersion) + assert.Equal(t, proto.SBOMSource_SBOM_SOURCE_WORKLOAD, in.Source) + assert.NotNil(t, in.Sbom) + return &proto.PutSBOMResponse{Success: true}, nil + }, + } + client.protoClient = mockClient + + resp, err := client.PutSBOM(context.Background(), imageDigest, syftVersion, proto.SBOMSource_SBOM_SOURCE_WORKLOAD, &v1beta1.SBOMSyft{}) + require.NoError(t, err) + assert.True(t, resp.Success) +} + +func TestStorageClient_GetSBOM(t *testing.T) { + client, err := NewStorageClient("grpc://storage.example.com:50051", "test-account", "test-key", "test-cluster") + require.NoError(t, err) + + tests := []struct { + name string + imageDigest string + syftVersion string + metadataOnly bool + exists bool + }{ + { + name: "metadata-only probe, hit", + imageDigest: "abc123", + syftVersion: "1.0.0", + metadataOnly: true, + exists: true, + }, + { + name: "metadata-only probe, miss", + imageDigest: "deadbeef", + syftVersion: "1.0.0", + metadataOnly: true, + exists: false, + }, + { + name: "full fetch, hit", + imageDigest: "abc123", + syftVersion: "1.0.0", + metadataOnly: false, + exists: true, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + mockClient := &mockStorageServiceClient{ + getSBOMFunc: func(ctx context.Context, in *proto.GetSBOMRequest, opts ...grpc.CallOption) (*proto.GetSBOMResponse, error) { + assert.Equal(t, tc.imageDigest, in.ImageDigest) + assert.Equal(t, tc.syftVersion, in.SyftVersion) + assert.Equal(t, tc.metadataOnly, in.MetadataOnly) + resp := &proto.GetSBOMResponse{Success: true, Exists: tc.exists} + if tc.exists { + resp.Metadata = &proto.SBOMMetadata{ + ImageDigest: tc.imageDigest, + SyftVersion: tc.syftVersion, + } + if !tc.metadataOnly { + resp.Sbom = &v1beta1.SBOMSyft{} + } + } + return resp, nil + }, + } + client.protoClient = mockClient + + resp, err := client.GetSBOM(context.Background(), tc.imageDigest, tc.syftVersion, tc.metadataOnly) + require.NoError(t, err) + assert.True(t, resp.Success) + assert.Equal(t, tc.exists, resp.Exists) + if tc.exists { + assert.NotNil(t, resp.Metadata) + if tc.metadataOnly { + assert.Nil(t, resp.Sbom) + } else { + assert.NotNil(t, resp.Sbom) + } + } + }) + } +} From 6ba08ada4e27a7bff57b8476e93b4ed2fdd1fbef Mon Sep 17 00:00:00 2001 From: jnathangreeg Date: Tue, 12 May 2026 11:05:10 +0300 Subject: [PATCH 2/6] NAUT-1310: collapse SBOM RPCs to single streaming + io.Reader API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses matthyx review on #50: - Drop unary PutSBOM and GetSBOM RPCs. There is now ONE client-streaming PutSBOM and ONE server-streaming GetSBOM — callers never need to know payload size in advance. Single-chunk streams handle the small-payload case naturally. - Client wrappers take/return io.Reader instead of *v1beta1.SBOMSyft, so callers can stream from any source (a file, a syft pipe, etc.) without holding the full marshaled blob in memory. Convenience helpers MarshalSBOM and UnmarshalSBOM cover the typed-object case. - Add a bufconn-based round-trip test (TestStorageClient_SBOMRoundTrip) that stands up a real gRPC server in-process and exercises the full marshal → chunk → wire → unchunk → unmarshal path with a non-trivial SBOMSyft. Covers: probe hit, probe miss, full fetch single-chunk, full fetch with the payload split across many small chunks (stresses the io.ReadCloser buffering logic). This is the kind of end-to-end test matthyx asked for — would have caught any wire-shape regression that the prior mock-based smoke tests missed. Co-Authored-By: Claude Opus 4.7 (1M context) --- pkg/client/v1/proto/storage_service.pb.go | 352 ++++----------- pkg/client/v1/proto/storage_service.proto | 96 ++--- .../v1/proto/storage_service_grpc.pb.go | 190 +++------ pkg/client/v1/storageclient.go | 239 ++++++----- pkg/client/v1/storageclient_test.go | 402 +++++++++++++----- 5 files changed, 607 insertions(+), 672 deletions(-) diff --git a/pkg/client/v1/proto/storage_service.pb.go b/pkg/client/v1/proto/storage_service.pb.go index 7e6388f..e3ae830 100644 --- a/pkg/client/v1/proto/storage_service.pb.go +++ b/pkg/client/v1/proto/storage_service.pb.go @@ -673,80 +673,6 @@ func (m *ListNetworkNeighborhoodsResponse) GetCont() string { return "" } -// PutSBOMRequest uploads an SBOM blob plus the keys identifying it. -// customer_guid, cluster, host_type, and host_id are sent via gRPC metadata -// headers (see the AuthInterceptor on the server side). The unary path is -// intended for SBOMs up to ~4 MiB; use PutSBOMStream for larger payloads. -type PutSBOMRequest struct { - // image_digest is the SHA-256 digest of the scanned image (without the - // "sha256:" prefix). Part of the primary key. - ImageDigest string `protobuf:"bytes,1,opt,name=image_digest,json=imageDigest,proto3" json:"image_digest,omitempty"` - // syft_version is the version of syft that produced the blob. Part of the - // primary key — multiple syft versions can coexist for one - // (customer_guid, image_digest) during gradual fleet upgrades. - SyftVersion string `protobuf:"bytes,2,opt,name=syft_version,json=syftVersion,proto3" json:"syft_version,omitempty"` - // source identifies how this SBOM was produced. - Source SBOMSource `protobuf:"varint,3,opt,name=source,proto3,enum=storageserver.v1.SBOMSource" json:"source,omitempty"` - // sbom is the SBOM payload as a SBOMSyft K8s resource (the same shape - // produced by syft + v1beta1.StripSBOM in node-agent / kubevuln). - Sbom *v1beta1.SBOMSyft `protobuf:"bytes,4,opt,name=sbom,proto3" json:"sbom,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *PutSBOMRequest) Reset() { *m = PutSBOMRequest{} } -func (m *PutSBOMRequest) String() string { return proto.CompactTextString(m) } -func (*PutSBOMRequest) ProtoMessage() {} -func (*PutSBOMRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_3d90829bc66d9c54, []int{8} -} -func (m *PutSBOMRequest) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_PutSBOMRequest.Unmarshal(m, b) -} -func (m *PutSBOMRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_PutSBOMRequest.Marshal(b, m, deterministic) -} -func (m *PutSBOMRequest) XXX_Merge(src proto.Message) { - xxx_messageInfo_PutSBOMRequest.Merge(m, src) -} -func (m *PutSBOMRequest) XXX_Size() int { - return xxx_messageInfo_PutSBOMRequest.Size(m) -} -func (m *PutSBOMRequest) XXX_DiscardUnknown() { - xxx_messageInfo_PutSBOMRequest.DiscardUnknown(m) -} - -var xxx_messageInfo_PutSBOMRequest proto.InternalMessageInfo - -func (m *PutSBOMRequest) GetImageDigest() string { - if m != nil { - return m.ImageDigest - } - return "" -} - -func (m *PutSBOMRequest) GetSyftVersion() string { - if m != nil { - return m.SyftVersion - } - return "" -} - -func (m *PutSBOMRequest) GetSource() SBOMSource { - if m != nil { - return m.Source - } - return SBOMSource_SBOM_SOURCE_UNSPECIFIED -} - -func (m *PutSBOMRequest) GetSbom() *v1beta1.SBOMSyft { - if m != nil { - return m.Sbom - } - return nil -} - // PutSBOMResponse indicates success or failure of the operation. S3 + // Postgres persistence happens asynchronously via Pulsar; success here // means the upload was accepted and the Pulsar message was published. @@ -763,7 +689,7 @@ func (m *PutSBOMResponse) Reset() { *m = PutSBOMResponse{} } func (m *PutSBOMResponse) String() string { return proto.CompactTextString(m) } func (*PutSBOMResponse) ProtoMessage() {} func (*PutSBOMResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_3d90829bc66d9c54, []int{9} + return fileDescriptor_3d90829bc66d9c54, []int{8} } func (m *PutSBOMResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_PutSBOMResponse.Unmarshal(m, b) @@ -806,12 +732,12 @@ func (m *PutSBOMResponse) GetErrorCode() ErrorCode { // PutSBOMChunk is a single chunk of a streamed SBOM upload. The first // chunk in the stream MUST set metadata. Subsequent chunks set only -// blob_chunk. Concatenated blob_chunk values form the serialized SBOMSyft +// blob_chunk. Concatenated blob_chunk values form the marshaled SBOMSyft // proto payload. type PutSBOMChunk struct { // metadata is set on the first chunk only. Metadata *PutSBOMChunkMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` - // blob_chunk is a slice of the serialized SBOMSyft payload. + // blob_chunk is a slice of the marshaled SBOMSyft payload. BlobChunk []byte `protobuf:"bytes,2,opt,name=blob_chunk,json=blobChunk,proto3" json:"blob_chunk,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` @@ -822,7 +748,7 @@ func (m *PutSBOMChunk) Reset() { *m = PutSBOMChunk{} } func (m *PutSBOMChunk) String() string { return proto.CompactTextString(m) } func (*PutSBOMChunk) ProtoMessage() {} func (*PutSBOMChunk) Descriptor() ([]byte, []int) { - return fileDescriptor_3d90829bc66d9c54, []int{10} + return fileDescriptor_3d90829bc66d9c54, []int{9} } func (m *PutSBOMChunk) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_PutSBOMChunk.Unmarshal(m, b) @@ -857,7 +783,7 @@ func (m *PutSBOMChunk) GetBlobChunk() []byte { } // PutSBOMChunkMetadata is the metadata header sent on the first chunk of a -// PutSBOMStream call. +// PutSBOM call. type PutSBOMChunkMetadata struct { ImageDigest string `protobuf:"bytes,1,opt,name=image_digest,json=imageDigest,proto3" json:"image_digest,omitempty"` SyftVersion string `protobuf:"bytes,2,opt,name=syft_version,json=syftVersion,proto3" json:"syft_version,omitempty"` @@ -871,7 +797,7 @@ func (m *PutSBOMChunkMetadata) Reset() { *m = PutSBOMChunkMetadata{} } func (m *PutSBOMChunkMetadata) String() string { return proto.CompactTextString(m) } func (*PutSBOMChunkMetadata) ProtoMessage() {} func (*PutSBOMChunkMetadata) Descriptor() ([]byte, []int) { - return fileDescriptor_3d90829bc66d9c54, []int{11} + return fileDescriptor_3d90829bc66d9c54, []int{10} } func (m *PutSBOMChunkMetadata) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_PutSBOMChunkMetadata.Unmarshal(m, b) @@ -935,7 +861,7 @@ func (m *GetSBOMRequest) Reset() { *m = GetSBOMRequest{} } func (m *GetSBOMRequest) String() string { return proto.CompactTextString(m) } func (*GetSBOMRequest) ProtoMessage() {} func (*GetSBOMRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_3d90829bc66d9c54, []int{12} + return fileDescriptor_3d90829bc66d9c54, []int{11} } func (m *GetSBOMRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_GetSBOMRequest.Unmarshal(m, b) @@ -976,96 +902,12 @@ func (m *GetSBOMRequest) GetMetadataOnly() bool { return false } -// GetSBOMResponse returns the SBOM (or just metadata). -type GetSBOMResponse struct { - Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` - ErrorMessage string `protobuf:"bytes,2,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` - ErrorCode ErrorCode `protobuf:"varint,3,opt,name=error_code,json=errorCode,proto3,enum=storageserver.v1.ErrorCode" json:"error_code,omitempty"` - // exists indicates whether an SBOM row was found for the requested key. - // When false, success is still true and metadata + sbom are empty. - Exists bool `protobuf:"varint,4,opt,name=exists,proto3" json:"exists,omitempty"` - // metadata is populated whenever exists=true. - Metadata *SBOMMetadata `protobuf:"bytes,5,opt,name=metadata,proto3" json:"metadata,omitempty"` - // sbom is populated only when exists=true and metadata_only was false on - // the request. - Sbom *v1beta1.SBOMSyft `protobuf:"bytes,6,opt,name=sbom,proto3" json:"sbom,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` -} - -func (m *GetSBOMResponse) Reset() { *m = GetSBOMResponse{} } -func (m *GetSBOMResponse) String() string { return proto.CompactTextString(m) } -func (*GetSBOMResponse) ProtoMessage() {} -func (*GetSBOMResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_3d90829bc66d9c54, []int{13} -} -func (m *GetSBOMResponse) XXX_Unmarshal(b []byte) error { - return xxx_messageInfo_GetSBOMResponse.Unmarshal(m, b) -} -func (m *GetSBOMResponse) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { - return xxx_messageInfo_GetSBOMResponse.Marshal(b, m, deterministic) -} -func (m *GetSBOMResponse) XXX_Merge(src proto.Message) { - xxx_messageInfo_GetSBOMResponse.Merge(m, src) -} -func (m *GetSBOMResponse) XXX_Size() int { - return xxx_messageInfo_GetSBOMResponse.Size(m) -} -func (m *GetSBOMResponse) XXX_DiscardUnknown() { - xxx_messageInfo_GetSBOMResponse.DiscardUnknown(m) -} - -var xxx_messageInfo_GetSBOMResponse proto.InternalMessageInfo - -func (m *GetSBOMResponse) GetSuccess() bool { - if m != nil { - return m.Success - } - return false -} - -func (m *GetSBOMResponse) GetErrorMessage() string { - if m != nil { - return m.ErrorMessage - } - return "" -} - -func (m *GetSBOMResponse) GetErrorCode() ErrorCode { - if m != nil { - return m.ErrorCode - } - return ErrorCode_ERROR_CODE_UNSPECIFIED -} - -func (m *GetSBOMResponse) GetExists() bool { - if m != nil { - return m.Exists - } - return false -} - -func (m *GetSBOMResponse) GetMetadata() *SBOMMetadata { - if m != nil { - return m.Metadata - } - return nil -} - -func (m *GetSBOMResponse) GetSbom() *v1beta1.SBOMSyft { - if m != nil { - return m.Sbom - } - return nil -} - -// GetSBOMChunk is a single chunk of a streamed SBOM download. The first +// GetSBOMChunk is a single chunk of a streamed SBOM response. The first // chunk sets metadata + status; subsequent chunks set only blob_chunk. type GetSBOMChunk struct { // metadata is set on the first chunk only. Metadata *GetSBOMChunkMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` - // blob_chunk is a slice of the serialized SBOMSyft payload. + // blob_chunk is a slice of the marshaled SBOMSyft payload. BlobChunk []byte `protobuf:"bytes,2,opt,name=blob_chunk,json=blobChunk,proto3" json:"blob_chunk,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` @@ -1076,7 +918,7 @@ func (m *GetSBOMChunk) Reset() { *m = GetSBOMChunk{} } func (m *GetSBOMChunk) String() string { return proto.CompactTextString(m) } func (*GetSBOMChunk) ProtoMessage() {} func (*GetSBOMChunk) Descriptor() ([]byte, []int) { - return fileDescriptor_3d90829bc66d9c54, []int{14} + return fileDescriptor_3d90829bc66d9c54, []int{12} } func (m *GetSBOMChunk) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_GetSBOMChunk.Unmarshal(m, b) @@ -1111,8 +953,9 @@ func (m *GetSBOMChunk) GetBlobChunk() []byte { } // GetSBOMChunkMetadata is the status header sent on the first chunk of a -// GetSBOMStream call. If exists=false or success=false the server closes -// the stream without sending blob chunks. +// GetSBOM response. If exists=false, success=false, or the request set +// metadata_only=true, the server closes the stream without sending blob +// chunks. type GetSBOMChunkMetadata struct { Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` ErrorMessage string `protobuf:"bytes,2,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` @@ -1128,7 +971,7 @@ func (m *GetSBOMChunkMetadata) Reset() { *m = GetSBOMChunkMetadata{} } func (m *GetSBOMChunkMetadata) String() string { return proto.CompactTextString(m) } func (*GetSBOMChunkMetadata) ProtoMessage() {} func (*GetSBOMChunkMetadata) Descriptor() ([]byte, []int) { - return fileDescriptor_3d90829bc66d9c54, []int{15} + return fileDescriptor_3d90829bc66d9c54, []int{13} } func (m *GetSBOMChunkMetadata) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_GetSBOMChunkMetadata.Unmarshal(m, b) @@ -1208,7 +1051,7 @@ func (m *SBOMMetadata) Reset() { *m = SBOMMetadata{} } func (m *SBOMMetadata) String() string { return proto.CompactTextString(m) } func (*SBOMMetadata) ProtoMessage() {} func (*SBOMMetadata) Descriptor() ([]byte, []int) { - return fileDescriptor_3d90829bc66d9c54, []int{16} + return fileDescriptor_3d90829bc66d9c54, []int{14} } func (m *SBOMMetadata) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_SBOMMetadata.Unmarshal(m, b) @@ -1281,12 +1124,10 @@ func init() { proto.RegisterType((*ListApplicationProfilesResponse)(nil), "storageserver.v1.ListApplicationProfilesResponse") proto.RegisterType((*ListNetworkNeighborhoodsRequest)(nil), "storageserver.v1.ListNetworkNeighborhoodsRequest") proto.RegisterType((*ListNetworkNeighborhoodsResponse)(nil), "storageserver.v1.ListNetworkNeighborhoodsResponse") - proto.RegisterType((*PutSBOMRequest)(nil), "storageserver.v1.PutSBOMRequest") proto.RegisterType((*PutSBOMResponse)(nil), "storageserver.v1.PutSBOMResponse") proto.RegisterType((*PutSBOMChunk)(nil), "storageserver.v1.PutSBOMChunk") proto.RegisterType((*PutSBOMChunkMetadata)(nil), "storageserver.v1.PutSBOMChunkMetadata") proto.RegisterType((*GetSBOMRequest)(nil), "storageserver.v1.GetSBOMRequest") - proto.RegisterType((*GetSBOMResponse)(nil), "storageserver.v1.GetSBOMResponse") proto.RegisterType((*GetSBOMChunk)(nil), "storageserver.v1.GetSBOMChunk") proto.RegisterType((*GetSBOMChunkMetadata)(nil), "storageserver.v1.GetSBOMChunkMetadata") proto.RegisterType((*SBOMMetadata)(nil), "storageserver.v1.SBOMMetadata") @@ -1295,88 +1136,83 @@ func init() { func init() { proto.RegisterFile("storage_service.proto", fileDescriptor_3d90829bc66d9c54) } var fileDescriptor_3d90829bc66d9c54 = []byte{ - // 1318 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x58, 0x41, 0x73, 0xdb, 0x44, - 0x14, 0x46, 0x8e, 0xeb, 0xc4, 0x2f, 0x4e, 0xaa, 0x6e, 0xdd, 0xd4, 0xa4, 0x69, 0x70, 0x5d, 0xa6, - 0x93, 0xe9, 0x80, 0xdc, 0x04, 0x0e, 0x4c, 0x6f, 0x8e, 0xad, 0xa6, 0x9e, 0x3a, 0x56, 0xba, 0xb2, - 0xdb, 0xa1, 0x17, 0x8d, 0x2c, 0x6f, 0x1c, 0x8d, 0x6d, 0xc9, 0xd5, 0xae, 0x53, 0x52, 0x66, 0x18, - 0xb8, 0x30, 0x30, 0x70, 0xe2, 0xc6, 0x8d, 0x1b, 0x07, 0x0e, 0xdc, 0xe9, 0x81, 0x61, 0x86, 0xbf, - 0x03, 0x7f, 0x01, 0x46, 0xab, 0x95, 0xa3, 0xd8, 0xb2, 0x69, 0x66, 0x1a, 0x0c, 0x27, 0x6b, 0xbf, - 0xf7, 0xf6, 0xbd, 0xcf, 0xef, 0x7d, 0x5a, 0x3d, 0x09, 0xae, 0x51, 0xe6, 0x7a, 0x66, 0x87, 0x18, - 0x94, 0x78, 0xc7, 0xb6, 0x45, 0x94, 0x81, 0xe7, 0x32, 0x17, 0xc9, 0x02, 0xf6, 0x51, 0xe2, 0x29, - 0xc7, 0xdb, 0xeb, 0x8f, 0x3b, 0x36, 0x3b, 0x1a, 0xb6, 0x14, 0xcb, 0xed, 0x17, 0xbb, 0xc3, 0x16, - 0xa1, 0x96, 0x39, 0x20, 0x45, 0xe1, 0x56, 0x1c, 0x74, 0x3b, 0x45, 0x73, 0x60, 0xd3, 0x22, 0x75, - 0x0f, 0xd9, 0x0b, 0xd3, 0x23, 0x96, 0xdb, 0x1f, 0xb8, 0xd4, 0x66, 0xb6, 0xeb, 0x14, 0x8f, 0xb7, - 0x5b, 0x84, 0x99, 0xdb, 0xc5, 0x0e, 0x71, 0x88, 0x67, 0x32, 0xd2, 0x0e, 0x92, 0x14, 0x7e, 0x90, - 0xe0, 0x86, 0x4e, 0x9c, 0x76, 0xd9, 0x75, 0x98, 0x69, 0x3b, 0xc4, 0x3b, 0xf0, 0xdc, 0x43, 0xbb, - 0x47, 0x30, 0x79, 0x3e, 0x24, 0x94, 0xa1, 0xcf, 0x25, 0xb8, 0x62, 0x85, 0x36, 0x63, 0x10, 0x18, - 0x73, 0x52, 0x5e, 0xda, 0x5a, 0xde, 0xd1, 0x95, 0x53, 0x3e, 0xca, 0x88, 0x8f, 0x22, 0xf8, 0x28, - 0x83, 0x6e, 0x47, 0xf1, 0xf9, 0x28, 0x31, 0x7c, 0x14, 0xc1, 0x47, 0x99, 0xc8, 0x2b, 0x5b, 0x63, - 0x48, 0xe1, 0x7b, 0x09, 0x36, 0xe2, 0x29, 0xd2, 0x81, 0xeb, 0x50, 0x82, 0x72, 0xb0, 0x48, 0x87, - 0x96, 0x45, 0x28, 0xe5, 0xc4, 0x96, 0x70, 0xb8, 0x44, 0xb7, 0x61, 0x85, 0x78, 0x9e, 0xeb, 0x19, - 0x7d, 0x42, 0xa9, 0xd9, 0x21, 0xb9, 0x44, 0x5e, 0xda, 0x4a, 0xe3, 0x0c, 0x07, 0xf7, 0x03, 0x0c, - 0xdd, 0x07, 0x08, 0x9c, 0x2c, 0xb7, 0x4d, 0x72, 0x0b, 0x79, 0x69, 0x6b, 0x75, 0xe7, 0x86, 0x32, - 0x5e, 0x7c, 0x45, 0xf5, 0x7d, 0xca, 0x6e, 0x9b, 0xe0, 0x34, 0x09, 0x2f, 0x0b, 0x3f, 0x49, 0x70, - 0x65, 0x8f, 0xb0, 0xb1, 0xa2, 0x21, 0x48, 0x76, 0x6d, 0xa7, 0xcd, 0xd9, 0xa4, 0x31, 0xbf, 0x46, - 0x1b, 0x90, 0x76, 0xcc, 0x3e, 0xa1, 0x03, 0xd3, 0x0a, 0x69, 0x9c, 0x02, 0xfe, 0x0e, 0x7f, 0xc1, - 0xb3, 0xa7, 0x31, 0xbf, 0x46, 0x6b, 0x90, 0xf2, 0x48, 0xc7, 0x76, 0x9d, 0x5c, 0x92, 0xa3, 0x62, - 0x85, 0x3e, 0x82, 0x9c, 0xd5, 0x73, 0x87, 0x6d, 0xc3, 0xb4, 0x2c, 0x77, 0xe8, 0x30, 0xc3, 0x6e, - 0x13, 0x87, 0xd9, 0x87, 0x36, 0xf1, 0x72, 0x97, 0xb8, 0xe7, 0x1a, 0xb7, 0x97, 0x02, 0x73, 0x75, - 0x64, 0x2d, 0xfc, 0x98, 0x04, 0x14, 0x65, 0x3b, 0xf7, 0xfa, 0xa1, 0x2f, 0x25, 0xb8, 0x6a, 0x0e, - 0x06, 0x3d, 0xdb, 0x32, 0x7d, 0x59, 0x8c, 0x04, 0x96, 0xe4, 0x02, 0x6b, 0xbe, 0x01, 0x81, 0x95, - 0x4e, 0xa3, 0x87, 0xff, 0x1b, 0x99, 0x13, 0x18, 0xfa, 0x5a, 0x82, 0xac, 0x43, 0xd8, 0x0b, 0xd7, - 0xeb, 0x1a, 0x0e, 0xb1, 0x3b, 0x47, 0x2d, 0xd7, 0x3b, 0x72, 0xdd, 0x36, 0xaf, 0xe8, 0xf2, 0xce, - 0x93, 0x37, 0xc0, 0xa4, 0x1e, 0x84, 0xaf, 0x47, 0xa2, 0xe3, 0xab, 0xce, 0x24, 0x38, 0xe5, 0x9e, - 0x4b, 0xfd, 0x9b, 0xf7, 0xdc, 0x2f, 0x12, 0x6c, 0xd6, 0x6c, 0xca, 0x26, 0xab, 0x47, 0x43, 0x91, - 0x9f, 0x11, 0xb4, 0x34, 0x2e, 0xe8, 0x2c, 0x5c, 0xea, 0xd9, 0x7d, 0x9b, 0xf1, 0x4e, 0x2e, 0xe0, - 0x60, 0xe1, 0xcb, 0xdc, 0x4f, 0x25, 0x64, 0xca, 0xaf, 0x23, 0x32, 0x4f, 0xbd, 0xb6, 0xcc, 0x17, - 0x67, 0xca, 0xfc, 0x55, 0x02, 0xde, 0x99, 0x4a, 0x7e, 0xfe, 0x9a, 0xff, 0x4a, 0x82, 0x6c, 0x8c, - 0xe6, 0x69, 0x2e, 0x99, 0x5f, 0xb8, 0x38, 0xd1, 0x5f, 0x9d, 0x14, 0x3d, 0x8d, 0xeb, 0x47, 0xe1, - 0x95, 0x14, 0x54, 0x2f, 0x46, 0xae, 0xff, 0x83, 0xde, 0xff, 0x9a, 0x80, 0xfc, 0x74, 0xf6, 0xf3, - 0x6f, 0xfe, 0x37, 0x12, 0x5c, 0x8b, 0x3b, 0x67, 0xc2, 0xee, 0x5f, 0xd4, 0x41, 0x93, 0x8d, 0x39, - 0x68, 0xe2, 0xfb, 0xff, 0x87, 0x04, 0xab, 0x07, 0x43, 0xa6, 0xef, 0x6a, 0xfb, 0x61, 0xbb, 0x6f, - 0x41, 0xc6, 0xee, 0xfb, 0x03, 0x4a, 0xdb, 0xee, 0x10, 0xca, 0x44, 0xc7, 0x97, 0x39, 0x56, 0xe1, - 0x90, 0xef, 0x42, 0x4f, 0x0e, 0x99, 0x71, 0x4c, 0x3c, 0xea, 0xf7, 0x33, 0xa8, 0xdb, 0xb2, 0x8f, - 0x3d, 0x09, 0x20, 0xf4, 0x21, 0xa4, 0xa8, 0x3b, 0xf4, 0xac, 0xb0, 0x64, 0x1b, 0x93, 0x25, 0xf3, - 0x93, 0xea, 0xdc, 0x07, 0x0b, 0x5f, 0x64, 0x40, 0x92, 0xb6, 0xdc, 0xbe, 0x78, 0x22, 0x3c, 0x7a, - 0x03, 0xe5, 0xe1, 0x49, 0x4e, 0x0e, 0x19, 0xe6, 0x81, 0x0b, 0xdf, 0x4a, 0x70, 0x79, 0xf4, 0x7f, - 0xe7, 0x3f, 0x51, 0x3c, 0x87, 0x8c, 0x60, 0x53, 0x3e, 0x1a, 0x3a, 0x5d, 0xb4, 0x0b, 0x4b, 0x7d, - 0xc2, 0xcc, 0xb6, 0xc9, 0x4c, 0x31, 0x76, 0xdd, 0x99, 0x8c, 0x14, 0xdd, 0xb1, 0x2f, 0xbc, 0xf1, - 0x68, 0x1f, 0xba, 0x09, 0xd0, 0xea, 0xb9, 0x2d, 0xc3, 0xf2, 0xed, 0x9c, 0x71, 0x06, 0xa7, 0x7d, - 0x84, 0x6f, 0x28, 0x7c, 0x27, 0x41, 0x36, 0x2e, 0xc2, 0x3c, 0xfb, 0x5e, 0xf8, 0x14, 0x56, 0xf7, - 0xc8, 0x05, 0xa8, 0xf0, 0x36, 0xac, 0x84, 0x75, 0x31, 0x5c, 0xa7, 0x77, 0xc2, 0x49, 0x2d, 0xe1, - 0x4c, 0x08, 0x6a, 0x4e, 0xef, 0xa4, 0xf0, 0x5b, 0x02, 0x2e, 0x8f, 0xb2, 0xcf, 0xff, 0xd0, 0x58, - 0x83, 0x14, 0xf9, 0xc4, 0xa6, 0x8c, 0xf2, 0xbb, 0x60, 0x09, 0x8b, 0x15, 0xba, 0x1f, 0xd1, 0x46, - 0x30, 0xa7, 0x6c, 0xc6, 0xd7, 0x36, 0x46, 0x13, 0xe1, 0x7d, 0x95, 0xba, 0xa8, 0xfb, 0xea, 0x39, - 0x64, 0x44, 0x09, 0xcf, 0x21, 0xe4, 0xe8, 0x8e, 0xf3, 0x0b, 0xf9, 0x4f, 0x09, 0xb2, 0x71, 0x11, - 0xfe, 0x8b, 0xbd, 0x2b, 0xc3, 0x8a, 0x5f, 0x26, 0xe3, 0x9c, 0x0d, 0xcc, 0xf8, 0x9b, 0xc2, 0x55, - 0xe1, 0x2f, 0x09, 0x32, 0x51, 0xf3, 0x5c, 0x4f, 0xea, 0x3b, 0x70, 0x99, 0x37, 0x87, 0xda, 0x2f, - 0x89, 0xd1, 0x3a, 0x61, 0x84, 0x8a, 0x01, 0x60, 0xc5, 0x87, 0x75, 0xfb, 0x25, 0xd9, 0xf5, 0x41, - 0xbf, 0x89, 0x96, 0x47, 0xfc, 0x77, 0x50, 0xc3, 0x0c, 0x1f, 0x3d, 0x69, 0x81, 0x94, 0x18, 0x7a, - 0x0f, 0x50, 0xcf, 0xa4, 0xcc, 0xf0, 0xc8, 0x21, 0xf1, 0x88, 0x63, 0x05, 0x6e, 0xc1, 0x7c, 0x20, - 0xfb, 0x16, 0x3c, 0x32, 0x94, 0xd8, 0xdd, 0xdf, 0x13, 0x90, 0x1e, 0xd5, 0x1d, 0xad, 0xc3, 0x9a, - 0x8a, 0xb1, 0x86, 0x8d, 0xb2, 0x56, 0x51, 0x8d, 0x66, 0x5d, 0x3f, 0x50, 0xcb, 0xd5, 0x07, 0x55, - 0xb5, 0x22, 0xbf, 0x85, 0x36, 0x61, 0x3d, 0x62, 0xab, 0xd6, 0x9f, 0x94, 0x6a, 0xd5, 0x8a, 0x81, - 0xd5, 0xc7, 0x4d, 0x55, 0x6f, 0xc8, 0x12, 0xba, 0x01, 0xd7, 0xcf, 0xec, 0x2d, 0x35, 0x1b, 0x0f, - 0x35, 0x5c, 0x7d, 0xa6, 0x56, 0xe4, 0x04, 0xca, 0xc3, 0x46, 0xc4, 0x78, 0x80, 0xb5, 0x07, 0xd5, - 0x9a, 0x6a, 0x34, 0x34, 0xcd, 0xa8, 0x95, 0xf0, 0x9e, 0x2a, 0x2f, 0x4c, 0xf1, 0x28, 0x6b, 0xfb, - 0x07, 0x35, 0xb5, 0xa1, 0x56, 0xe4, 0xe4, 0x14, 0x8f, 0xba, 0xd6, 0x30, 0x1e, 0x68, 0xcd, 0x7a, - 0x45, 0xbe, 0x84, 0x6e, 0xc2, 0xdb, 0x67, 0x28, 0x36, 0x54, 0x5c, 0x2f, 0xd5, 0x0c, 0x8e, 0xc9, - 0xa9, 0x31, 0x86, 0x07, 0xcd, 0x9a, 0x5e, 0xc2, 0xc2, 0xb8, 0x38, 0xb6, 0xd7, 0x6f, 0x4f, 0x24, - 0xf4, 0x52, 0x9c, 0xf9, 0x94, 0x7d, 0xfa, 0x2e, 0x05, 0x38, 0xed, 0xa8, 0x9f, 0x88, 0x7b, 0xe8, - 0x5a, 0x13, 0x97, 0xc7, 0xeb, 0x98, 0x83, 0x6c, 0xd4, 0xf8, 0x54, 0xc3, 0x8f, 0x6a, 0x5a, 0xa9, - 0x22, 0x4b, 0xe3, 0x16, 0xac, 0xee, 0x55, 0xf5, 0x06, 0xfe, 0x58, 0x4e, 0xa0, 0x2c, 0xc8, 0x51, - 0xcb, 0x43, 0x4d, 0x6f, 0xc8, 0x0b, 0x3b, 0x3f, 0xa7, 0x60, 0x55, 0x0f, 0x84, 0xa5, 0x07, 0x5f, - 0x3e, 0xd0, 0x10, 0xb2, 0x71, 0xaf, 0xfa, 0xe8, 0xfd, 0x18, 0x05, 0x4e, 0xff, 0x6a, 0xb1, 0xae, - 0xbc, 0xae, 0xbb, 0x38, 0xdb, 0x9f, 0x02, 0x9c, 0xbe, 0x17, 0xa3, 0xdb, 0xb1, 0xe7, 0xd2, 0x58, - 0x8a, 0x77, 0x67, 0x3b, 0x89, 0xc0, 0x9f, 0xc1, 0xf5, 0x29, 0x6f, 0x22, 0xe8, 0xde, 0x64, 0x80, - 0xd9, 0x6f, 0x5c, 0xeb, 0xdb, 0xe7, 0xd8, 0x21, 0xf2, 0x7f, 0x21, 0x41, 0x6e, 0xda, 0x38, 0x8c, - 0xa6, 0xc4, 0x9b, 0x31, 0xf8, 0xaf, 0xef, 0x9c, 0x67, 0x8b, 0xe0, 0x50, 0x87, 0x45, 0x31, 0x5d, - 0xa0, 0xfc, 0xd4, 0xd1, 0x25, 0x4c, 0x70, 0x6b, 0x86, 0x87, 0x88, 0xd7, 0x80, 0x15, 0x01, 0xe9, - 0xcc, 0x23, 0x66, 0x1f, 0x6d, 0xce, 0x1e, 0x88, 0x5e, 0x23, 0xe6, 0x96, 0xe4, 0xb3, 0x14, 0x8f, - 0x8e, 0x38, 0x96, 0x67, 0x47, 0x91, 0xb8, 0x88, 0xe3, 0xe3, 0x82, 0x0e, 0x2b, 0x02, 0x12, 0x2c, - 0xff, 0x39, 0xea, 0xe6, 0xec, 0xe7, 0xe1, 0x3d, 0x69, 0x77, 0xe7, 0xd9, 0xbd, 0xd8, 0x4f, 0x80, - 0x2d, 0xd3, 0xea, 0x12, 0xa7, 0xcd, 0x3f, 0x01, 0x5a, 0x3d, 0x9b, 0x38, 0xac, 0x78, 0xbc, 0x5d, - 0xe4, 0x5f, 0xf8, 0x5a, 0x29, 0xfe, 0xf3, 0xc1, 0xdf, 0x01, 0x00, 0x00, 0xff, 0xff, 0x2f, 0x41, - 0xbc, 0x5a, 0x66, 0x14, 0x00, 0x00, + // 1244 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x58, 0x4d, 0x6f, 0x1b, 0xc5, + 0x1b, 0xff, 0x6f, 0x5e, 0x9c, 0xf8, 0x89, 0xd3, 0x6e, 0xa7, 0x6e, 0xea, 0xbf, 0x9b, 0x06, 0xd7, + 0x45, 0x55, 0x54, 0xc1, 0xba, 0x09, 0x1c, 0x10, 0x37, 0xc7, 0xde, 0xa6, 0x16, 0x8e, 0xd7, 0x9d, + 0xb5, 0x5b, 0xd1, 0xcb, 0x6a, 0xbd, 0x9e, 0x38, 0x2b, 0xdb, 0x3b, 0xee, 0xce, 0x38, 0x25, 0x45, + 0x42, 0x20, 0x24, 0x04, 0x82, 0x13, 0x37, 0x6e, 0xdc, 0x38, 0xf0, 0x0d, 0xe8, 0x81, 0x0b, 0x9f, + 0x87, 0xaf, 0x00, 0x9a, 0xd9, 0x5d, 0xdb, 0xb5, 0xd7, 0x56, 0x23, 0xa5, 0x04, 0x4e, 0xde, 0xf9, + 0x3d, 0x2f, 0xf3, 0xf3, 0xf3, 0xfc, 0x76, 0x5e, 0x16, 0x6e, 0x30, 0x4e, 0x7d, 0xbb, 0x43, 0x2c, + 0x46, 0xfc, 0x53, 0xd7, 0x21, 0xda, 0xc0, 0xa7, 0x9c, 0x22, 0x35, 0x84, 0x05, 0x4a, 0x7c, 0xed, + 0x74, 0x2f, 0xfb, 0xb8, 0xe3, 0xf2, 0x93, 0x61, 0x4b, 0x73, 0x68, 0xbf, 0xd0, 0x1d, 0xb6, 0x08, + 0x73, 0xec, 0x01, 0x29, 0x84, 0x6e, 0x85, 0x41, 0xb7, 0x53, 0xb0, 0x07, 0x2e, 0x2b, 0x30, 0x7a, + 0xcc, 0x5f, 0xd8, 0x3e, 0x71, 0x68, 0x7f, 0x40, 0x99, 0xcb, 0x5d, 0xea, 0x15, 0x4e, 0xf7, 0x5a, + 0x84, 0xdb, 0x7b, 0x85, 0x0e, 0xf1, 0x88, 0x6f, 0x73, 0xd2, 0x0e, 0x26, 0xc9, 0xff, 0xac, 0xc0, + 0x2d, 0x93, 0x78, 0xed, 0x12, 0xf5, 0xb8, 0xed, 0x7a, 0xc4, 0xaf, 0xfb, 0xf4, 0xd8, 0xed, 0x11, + 0x4c, 0x9e, 0x0f, 0x09, 0xe3, 0xe8, 0x4b, 0x05, 0xae, 0x39, 0x91, 0xcd, 0x1a, 0x04, 0xc6, 0x8c, + 0x92, 0x53, 0x76, 0x37, 0xf6, 0x4d, 0x6d, 0xcc, 0x47, 0x1b, 0xf1, 0xd1, 0x42, 0x3e, 0xda, 0xa0, + 0xdb, 0xd1, 0x04, 0x1f, 0x2d, 0x86, 0x8f, 0x16, 0xf2, 0xd1, 0x66, 0xe6, 0x55, 0x9d, 0x29, 0x24, + 0xff, 0x93, 0x02, 0xdb, 0xf1, 0x14, 0xd9, 0x80, 0x7a, 0x8c, 0xa0, 0x0c, 0xac, 0xb1, 0xa1, 0xe3, + 0x10, 0xc6, 0x24, 0xb1, 0x75, 0x1c, 0x0d, 0xd1, 0x5d, 0xd8, 0x24, 0xbe, 0x4f, 0x7d, 0xab, 0x4f, + 0x18, 0xb3, 0x3b, 0x24, 0xb3, 0x94, 0x53, 0x76, 0x93, 0x38, 0x25, 0xc1, 0xa3, 0x00, 0x43, 0x1f, + 0x03, 0x04, 0x4e, 0x0e, 0x6d, 0x93, 0xcc, 0x72, 0x4e, 0xd9, 0xbd, 0xb2, 0x7f, 0x4b, 0x9b, 0x2e, + 0xbe, 0xa6, 0x0b, 0x9f, 0x12, 0x6d, 0x13, 0x9c, 0x24, 0xd1, 0x63, 0xfe, 0x57, 0x05, 0xae, 0x1d, + 0x12, 0x3e, 0x55, 0x34, 0x04, 0x2b, 0x5d, 0xd7, 0x6b, 0x4b, 0x36, 0x49, 0x2c, 0x9f, 0xd1, 0x36, + 0x24, 0x3d, 0xbb, 0x4f, 0xd8, 0xc0, 0x76, 0x22, 0x1a, 0x63, 0x40, 0x44, 0x88, 0x81, 0x9c, 0x3d, + 0x89, 0xe5, 0x33, 0xda, 0x82, 0x84, 0x4f, 0x3a, 0x2e, 0xf5, 0x32, 0x2b, 0x12, 0x0d, 0x47, 0xe8, + 0x23, 0xc8, 0x38, 0x3d, 0x3a, 0x6c, 0x5b, 0xb6, 0xe3, 0xd0, 0xa1, 0xc7, 0x2d, 0xb7, 0x4d, 0x3c, + 0xee, 0x1e, 0xbb, 0xc4, 0xcf, 0xac, 0x4a, 0xcf, 0x2d, 0x69, 0x2f, 0x06, 0xe6, 0xca, 0xc8, 0x9a, + 0xff, 0x65, 0x05, 0xd0, 0x24, 0xdb, 0x4b, 0xaf, 0x1f, 0xfa, 0x46, 0x81, 0xeb, 0xf6, 0x60, 0xd0, + 0x73, 0x1d, 0x5b, 0xc8, 0x62, 0x24, 0xb0, 0x15, 0x29, 0xb0, 0xe6, 0x05, 0x08, 0xac, 0x38, 0xce, + 0x1e, 0xfd, 0x6f, 0x64, 0xcf, 0x60, 0xe8, 0x3b, 0x05, 0xd2, 0x1e, 0xe1, 0x2f, 0xa8, 0xdf, 0xb5, + 0x3c, 0xe2, 0x76, 0x4e, 0x5a, 0xd4, 0x3f, 0xa1, 0xb4, 0x2d, 0x2b, 0xba, 0xb1, 0xff, 0xe4, 0x02, + 0x98, 0xd4, 0x82, 0xf4, 0xb5, 0x89, 0xec, 0xf8, 0xba, 0x37, 0x0b, 0xce, 0x79, 0xe7, 0x12, 0xff, + 0xe4, 0x3b, 0xf7, 0x9b, 0x02, 0x3b, 0x55, 0x97, 0xf1, 0xd9, 0xea, 0xb1, 0x48, 0xe4, 0xaf, 0x09, + 0x5a, 0x99, 0x16, 0x74, 0x1a, 0x56, 0x7b, 0x6e, 0xdf, 0xe5, 0xb2, 0x93, 0xcb, 0x38, 0x18, 0x08, + 0x99, 0x8b, 0xa9, 0x42, 0x99, 0xca, 0xe7, 0x09, 0x99, 0x27, 0xde, 0x58, 0xe6, 0x6b, 0x0b, 0x65, + 0xfe, 0x6a, 0x09, 0xde, 0x99, 0x4b, 0xfe, 0xf2, 0x35, 0xff, 0xad, 0x02, 0xe9, 0x18, 0xcd, 0xb3, + 0xcc, 0x4a, 0x6e, 0xf9, 0xed, 0x89, 0xfe, 0xfa, 0xac, 0xe8, 0x59, 0x5c, 0x3f, 0xf2, 0xaf, 0x94, + 0xa0, 0x7a, 0x31, 0x72, 0xfd, 0x0f, 0xf4, 0xfe, 0xf7, 0x25, 0xc8, 0xcd, 0x67, 0x7f, 0xf9, 0xcd, + 0xff, 0x5e, 0x81, 0x1b, 0x71, 0xeb, 0x4c, 0xd4, 0xfd, 0xb7, 0xb5, 0xd0, 0xa4, 0x63, 0x16, 0x9a, + 0xf8, 0xfe, 0xff, 0xa0, 0xc0, 0xd5, 0xfa, 0x90, 0x9b, 0x07, 0xc6, 0xd1, 0xbf, 0x61, 0x87, 0x7d, + 0x0e, 0xa9, 0x90, 0x4d, 0xe9, 0x64, 0xe8, 0x75, 0xd1, 0x01, 0xac, 0xf7, 0x09, 0xb7, 0xdb, 0x36, + 0xb7, 0xc3, 0x63, 0xc8, 0xbd, 0xd9, 0x4c, 0x93, 0x11, 0x47, 0xa1, 0x37, 0x1e, 0xc5, 0xa1, 0xdb, + 0x00, 0xad, 0x1e, 0x6d, 0x59, 0x8e, 0xb0, 0x4b, 0xc6, 0x29, 0x9c, 0x14, 0x88, 0x0c, 0xc8, 0xff, + 0xa8, 0x40, 0x3a, 0x2e, 0x03, 0xba, 0x03, 0x29, 0xb7, 0x2f, 0x0e, 0x6a, 0x6d, 0xb7, 0x43, 0x18, + 0x0f, 0x95, 0xbf, 0x21, 0xb1, 0xb2, 0x84, 0x84, 0x0b, 0x3b, 0x3b, 0xe6, 0xd6, 0x29, 0xf1, 0x99, + 0xd0, 0x75, 0x50, 0x8e, 0x0d, 0x81, 0x3d, 0x09, 0x20, 0xf4, 0x21, 0x24, 0x18, 0x1d, 0xfa, 0x4e, + 0x54, 0x89, 0xed, 0x59, 0xfe, 0x62, 0x6a, 0x53, 0xfa, 0xe0, 0xd0, 0x37, 0xff, 0x39, 0x5c, 0x39, + 0x24, 0x61, 0x57, 0x82, 0x97, 0xf0, 0x62, 0xd8, 0xdc, 0x85, 0xcd, 0xa8, 0x2e, 0x16, 0xf5, 0x7a, + 0x67, 0x92, 0xd4, 0x3a, 0x4e, 0x45, 0xa0, 0xe1, 0xf5, 0xce, 0x44, 0x13, 0xc2, 0xc9, 0xcf, 0xd1, + 0x84, 0xc9, 0x88, 0xf3, 0x37, 0xe1, 0x4f, 0x05, 0xd2, 0x71, 0x19, 0x2e, 0xf3, 0xe5, 0xdd, 0x82, + 0x04, 0xf9, 0xcc, 0x65, 0x9c, 0xc9, 0x95, 0x6d, 0x1d, 0x87, 0x23, 0x54, 0x82, 0x4d, 0xd6, 0xa2, + 0x7d, 0x6b, 0x54, 0x93, 0xe0, 0xd0, 0xb0, 0x13, 0xdf, 0xd8, 0x51, 0x2d, 0x52, 0x22, 0x28, 0x1a, + 0xe5, 0xff, 0x52, 0x20, 0x35, 0x69, 0xbe, 0x4c, 0xb5, 0xa1, 0x7b, 0x70, 0x55, 0x36, 0x87, 0xb9, + 0x2f, 0x89, 0xd5, 0x3a, 0xe3, 0x84, 0x85, 0x8b, 0xf9, 0xa6, 0x80, 0x4d, 0xf7, 0x25, 0x39, 0x10, + 0xa0, 0x68, 0xa2, 0xe3, 0x13, 0x71, 0x9f, 0xb0, 0xec, 0x68, 0x19, 0x49, 0x86, 0x48, 0x91, 0xa3, + 0xf7, 0x00, 0xf5, 0x6c, 0xc6, 0x2d, 0x9f, 0x1c, 0x13, 0x9f, 0x78, 0x4e, 0xe0, 0x16, 0xac, 0xf5, + 0xaa, 0xb0, 0xe0, 0x91, 0xa1, 0xc8, 0xef, 0xff, 0xb1, 0x04, 0xc9, 0x51, 0xdd, 0x51, 0x16, 0xb6, + 0x74, 0x8c, 0x0d, 0x6c, 0x95, 0x8c, 0xb2, 0x6e, 0x35, 0x6b, 0x66, 0x5d, 0x2f, 0x55, 0x1e, 0x56, + 0xf4, 0xb2, 0xfa, 0x3f, 0xb4, 0x03, 0xd9, 0x09, 0x5b, 0xa5, 0xf6, 0xa4, 0x58, 0xad, 0x94, 0x2d, + 0xac, 0x3f, 0x6e, 0xea, 0x66, 0x43, 0x55, 0xd0, 0x2d, 0xb8, 0xf9, 0x5a, 0x6c, 0xb1, 0xd9, 0x78, + 0x64, 0xe0, 0xca, 0x33, 0xbd, 0xac, 0x2e, 0xa1, 0x1c, 0x6c, 0x4f, 0x18, 0xeb, 0xd8, 0x78, 0x58, + 0xa9, 0xea, 0x56, 0xc3, 0x30, 0xac, 0x6a, 0x11, 0x1f, 0xea, 0xea, 0xf2, 0x1c, 0x8f, 0x92, 0x71, + 0x54, 0xaf, 0xea, 0x0d, 0xbd, 0xac, 0xae, 0xcc, 0xf1, 0xa8, 0x19, 0x0d, 0xeb, 0xa1, 0xd1, 0xac, + 0x95, 0xd5, 0x55, 0x74, 0x1b, 0xfe, 0xff, 0x1a, 0xc5, 0x86, 0x8e, 0x6b, 0xc5, 0xaa, 0x25, 0x31, + 0x35, 0x31, 0xc5, 0xb0, 0xde, 0xac, 0x9a, 0x45, 0x1c, 0x1a, 0xd7, 0xa6, 0x62, 0x45, 0x7b, 0x26, + 0x52, 0xaf, 0xc7, 0x99, 0xc7, 0xec, 0x93, 0xf7, 0x19, 0xc0, 0xb8, 0xa3, 0x62, 0x22, 0xe9, 0x61, + 0x1a, 0x4d, 0x5c, 0x9a, 0xae, 0x63, 0x06, 0xd2, 0x93, 0xc6, 0xa7, 0x06, 0xfe, 0xa4, 0x6a, 0x14, + 0xcb, 0xaa, 0x32, 0x6d, 0xc1, 0xfa, 0x61, 0xc5, 0x6c, 0xe0, 0x4f, 0xd5, 0x25, 0x94, 0x06, 0x75, + 0xd2, 0xf2, 0xc8, 0x30, 0x1b, 0xea, 0xf2, 0xfe, 0xd7, 0xab, 0x70, 0xc5, 0x0c, 0x84, 0x65, 0x06, + 0xb7, 0x58, 0x34, 0x84, 0x74, 0xdc, 0xb5, 0x0d, 0xbd, 0x1f, 0xa3, 0xc0, 0xf9, 0x37, 0xd0, 0xac, + 0xf6, 0xa6, 0xee, 0xe1, 0x5e, 0xf5, 0x14, 0x60, 0x7c, 0xc7, 0x41, 0x77, 0x63, 0xd7, 0xa5, 0xa9, + 0x29, 0xde, 0x5d, 0xec, 0x14, 0x26, 0xfe, 0x02, 0x6e, 0xce, 0x39, 0x55, 0xa2, 0x07, 0xb3, 0x09, + 0x16, 0x9f, 0x9e, 0xb3, 0x7b, 0xe7, 0x88, 0x08, 0xe7, 0xff, 0x4a, 0x81, 0xcc, 0xbc, 0xa3, 0x0d, + 0x9a, 0x93, 0x6f, 0xc1, 0x21, 0x2e, 0xbb, 0x7f, 0x9e, 0x90, 0x90, 0x43, 0x0d, 0xd6, 0xc2, 0x9d, + 0x11, 0xed, 0x2c, 0xde, 0x76, 0xb3, 0x77, 0xe6, 0xda, 0xa3, 0x6c, 0xbb, 0x0a, 0x3a, 0x82, 0xb5, + 0x70, 0x91, 0x47, 0xb9, 0xb9, 0x3b, 0x48, 0x44, 0x78, 0x67, 0xf1, 0x1e, 0xf3, 0x40, 0x39, 0xd8, + 0x7f, 0xf6, 0x20, 0xf6, 0x13, 0x49, 0xcb, 0x76, 0xba, 0xc4, 0x6b, 0xcb, 0x4f, 0x24, 0x4e, 0xcf, + 0x25, 0x1e, 0x2f, 0x9c, 0xee, 0x15, 0xe4, 0x17, 0x90, 0x56, 0x42, 0xfe, 0x7c, 0xf0, 0x77, 0x00, + 0x00, 0x00, 0xff, 0xff, 0x93, 0x62, 0x6e, 0x2f, 0x86, 0x11, 0x00, 0x00, } diff --git a/pkg/client/v1/proto/storage_service.proto b/pkg/client/v1/proto/storage_service.proto index 7473602..fefe18e 100644 --- a/pkg/client/v1/proto/storage_service.proto +++ b/pkg/client/v1/proto/storage_service.proto @@ -22,30 +22,27 @@ service StorageService { // ListNetworkNeighborhoods lists all NetworkNeighborhoods in a namespace (returns metadata only, nil Spec) rpc ListNetworkNeighborhoods(ListNetworkNeighborhoodsRequest) returns (ListNetworkNeighborhoodsResponse); - // PutSBOM uploads an SBOM produced for an image. Idempotent on - // (customer_guid, image_digest, syft_version) — duplicate calls upsert the - // metadata row and overwrite the S3 blob; end-state is consistent. Used - // for SBOM payloads up to the default 4 MiB gRPC message limit. - rpc PutSBOM(PutSBOMRequest) returns (PutSBOMResponse); - - // PutSBOMStream uploads an SBOM larger than the default 4 MiB unary gRPC - // message limit, chunked client-side. The first chunk in the stream MUST - // set metadata (image_digest, syft_version, source); subsequent chunks - // set only blob_chunk. Concatenated chunks form the serialized SBOMSyft - // proto payload. - rpc PutSBOMStream(stream PutSBOMChunk) returns (PutSBOMResponse); + // PutSBOM uploads an SBOM produced for an image. Client-streaming so the + // caller need not know the payload size in advance and can stream bytes + // from any io.Reader. Idempotent on (customer_guid, image_digest, + // syft_version) — duplicate calls upsert the metadata row and overwrite + // the S3 blob; end-state is consistent. + // + // The first chunk MUST set metadata (image_digest, syft_version, source) + // and MAY include blob_chunk for small payloads. Subsequent chunks set + // only blob_chunk. Concatenated blob_chunk values form the marshaled + // SBOMSyft proto payload. + rpc PutSBOM(stream PutSBOMChunk) returns (PutSBOMResponse); // GetSBOM probes for or fetches an SBOM by (customer_guid, image_digest, - // syft_version). Set metadata_only=true to skip transferring the blob — - // used by agents to decide whether to regenerate. Returns the same - // envelope (with exists=false) on a miss. - rpc GetSBOM(GetSBOMRequest) returns (GetSBOMResponse); - - // GetSBOMStream returns an SBOM larger than the default 4 MiB unary gRPC - // message limit, server-streamed. The first chunk sets metadata + status; - // subsequent chunks set only blob_chunk. The metadata_only flag on the - // request is ignored — GetSBOM (unary) is the metadata-only entry point. - rpc GetSBOMStream(GetSBOMRequest) returns (stream GetSBOMChunk); + // syft_version). Server-streaming so the caller need not know the + // response size in advance. + // + // The first chunk in the response carries metadata + status. When the + // request sets metadata_only=true OR the row does not exist OR success + // is false, the server closes the stream after the metadata chunk. + // Otherwise subsequent chunks carry the marshaled SBOMSyft bytes. + rpc GetSBOM(GetSBOMRequest) returns (stream GetSBOMChunk); } // SendContainerProfileRequest contains the container profile to be stored @@ -206,28 +203,6 @@ message ListNetworkNeighborhoodsResponse { string cont = 5; } -// PutSBOMRequest uploads an SBOM blob plus the keys identifying it. -// customer_guid, cluster, host_type, and host_id are sent via gRPC metadata -// headers (see the AuthInterceptor on the server side). The unary path is -// intended for SBOMs up to ~4 MiB; use PutSBOMStream for larger payloads. -message PutSBOMRequest { - // image_digest is the SHA-256 digest of the scanned image (without the - // "sha256:" prefix). Part of the primary key. - string image_digest = 1; - - // syft_version is the version of syft that produced the blob. Part of the - // primary key — multiple syft versions can coexist for one - // (customer_guid, image_digest) during gradual fleet upgrades. - string syft_version = 2; - - // source identifies how this SBOM was produced. - SBOMSource source = 3; - - // sbom is the SBOM payload as a SBOMSyft K8s resource (the same shape - // produced by syft + v1beta1.StripSBOM in node-agent / kubevuln). - github.com.kubescape.storage.pkg.apis.softwarecomposition.v1beta1.SBOMSyft sbom = 4; -} - // PutSBOMResponse indicates success or failure of the operation. S3 + // Postgres persistence happens asynchronously via Pulsar; success here // means the upload was accepted and the Pulsar message was published. @@ -239,18 +214,18 @@ message PutSBOMResponse { // PutSBOMChunk is a single chunk of a streamed SBOM upload. The first // chunk in the stream MUST set metadata. Subsequent chunks set only -// blob_chunk. Concatenated blob_chunk values form the serialized SBOMSyft +// blob_chunk. Concatenated blob_chunk values form the marshaled SBOMSyft // proto payload. message PutSBOMChunk { // metadata is set on the first chunk only. PutSBOMChunkMetadata metadata = 1; - // blob_chunk is a slice of the serialized SBOMSyft payload. + // blob_chunk is a slice of the marshaled SBOMSyft payload. bytes blob_chunk = 2; } // PutSBOMChunkMetadata is the metadata header sent on the first chunk of a -// PutSBOMStream call. +// PutSBOM call. message PutSBOMChunkMetadata { string image_digest = 1; string syft_version = 2; @@ -275,37 +250,20 @@ message GetSBOMRequest { bool metadata_only = 3; } -// GetSBOMResponse returns the SBOM (or just metadata). -message GetSBOMResponse { - bool success = 1; - string error_message = 2; - ErrorCode error_code = 3; - - // exists indicates whether an SBOM row was found for the requested key. - // When false, success is still true and metadata + sbom are empty. - bool exists = 4; - - // metadata is populated whenever exists=true. - SBOMMetadata metadata = 5; - - // sbom is populated only when exists=true and metadata_only was false on - // the request. - github.com.kubescape.storage.pkg.apis.softwarecomposition.v1beta1.SBOMSyft sbom = 6; -} - -// GetSBOMChunk is a single chunk of a streamed SBOM download. The first +// GetSBOMChunk is a single chunk of a streamed SBOM response. The first // chunk sets metadata + status; subsequent chunks set only blob_chunk. message GetSBOMChunk { // metadata is set on the first chunk only. GetSBOMChunkMetadata metadata = 1; - // blob_chunk is a slice of the serialized SBOMSyft payload. + // blob_chunk is a slice of the marshaled SBOMSyft payload. bytes blob_chunk = 2; } // GetSBOMChunkMetadata is the status header sent on the first chunk of a -// GetSBOMStream call. If exists=false or success=false the server closes -// the stream without sending blob chunks. +// GetSBOM response. If exists=false, success=false, or the request set +// metadata_only=true, the server closes the stream without sending blob +// chunks. message GetSBOMChunkMetadata { bool success = 1; string error_message = 2; diff --git a/pkg/client/v1/proto/storage_service_grpc.pb.go b/pkg/client/v1/proto/storage_service_grpc.pb.go index e3f1ac0..545b62d 100644 --- a/pkg/client/v1/proto/storage_service_grpc.pb.go +++ b/pkg/client/v1/proto/storage_service_grpc.pb.go @@ -24,9 +24,7 @@ const ( StorageService_ListApplicationProfiles_FullMethodName = "/storageserver.v1.StorageService/ListApplicationProfiles" StorageService_ListNetworkNeighborhoods_FullMethodName = "/storageserver.v1.StorageService/ListNetworkNeighborhoods" StorageService_PutSBOM_FullMethodName = "/storageserver.v1.StorageService/PutSBOM" - StorageService_PutSBOMStream_FullMethodName = "/storageserver.v1.StorageService/PutSBOMStream" StorageService_GetSBOM_FullMethodName = "/storageserver.v1.StorageService/GetSBOM" - StorageService_GetSBOMStream_FullMethodName = "/storageserver.v1.StorageService/GetSBOMStream" ) // StorageServiceClient is the client API for StorageService service. @@ -45,27 +43,26 @@ type StorageServiceClient interface { ListApplicationProfiles(ctx context.Context, in *ListApplicationProfilesRequest, opts ...grpc.CallOption) (*ListApplicationProfilesResponse, error) // ListNetworkNeighborhoods lists all NetworkNeighborhoods in a namespace (returns metadata only, nil Spec) ListNetworkNeighborhoods(ctx context.Context, in *ListNetworkNeighborhoodsRequest, opts ...grpc.CallOption) (*ListNetworkNeighborhoodsResponse, error) - // PutSBOM uploads an SBOM produced for an image. Idempotent on - // (customer_guid, image_digest, syft_version) — duplicate calls upsert the - // metadata row and overwrite the S3 blob; end-state is consistent. Used - // for SBOM payloads up to the default 4 MiB gRPC message limit. - PutSBOM(ctx context.Context, in *PutSBOMRequest, opts ...grpc.CallOption) (*PutSBOMResponse, error) - // PutSBOMStream uploads an SBOM larger than the default 4 MiB unary gRPC - // message limit, chunked client-side. The first chunk in the stream MUST - // set metadata (image_digest, syft_version, source); subsequent chunks - // set only blob_chunk. Concatenated chunks form the serialized SBOMSyft - // proto payload. - PutSBOMStream(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[PutSBOMChunk, PutSBOMResponse], error) + // PutSBOM uploads an SBOM produced for an image. Client-streaming so the + // caller need not know the payload size in advance and can stream bytes + // from any io.Reader. Idempotent on (customer_guid, image_digest, + // syft_version) — duplicate calls upsert the metadata row and overwrite + // the S3 blob; end-state is consistent. + // + // The first chunk MUST set metadata (image_digest, syft_version, source) + // and MAY include blob_chunk for small payloads. Subsequent chunks set + // only blob_chunk. Concatenated blob_chunk values form the marshaled + // SBOMSyft proto payload. + PutSBOM(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[PutSBOMChunk, PutSBOMResponse], error) // GetSBOM probes for or fetches an SBOM by (customer_guid, image_digest, - // syft_version). Set metadata_only=true to skip transferring the blob — - // used by agents to decide whether to regenerate. Returns the same - // envelope (with exists=false) on a miss. - GetSBOM(ctx context.Context, in *GetSBOMRequest, opts ...grpc.CallOption) (*GetSBOMResponse, error) - // GetSBOMStream returns an SBOM larger than the default 4 MiB unary gRPC - // message limit, server-streamed. The first chunk sets metadata + status; - // subsequent chunks set only blob_chunk. The metadata_only flag on the - // request is ignored — GetSBOM (unary) is the metadata-only entry point. - GetSBOMStream(ctx context.Context, in *GetSBOMRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GetSBOMChunk], error) + // syft_version). Server-streaming so the caller need not know the + // response size in advance. + // + // The first chunk in the response carries metadata + status. When the + // request sets metadata_only=true OR the row does not exist OR success + // is false, the server closes the stream after the metadata chunk. + // Otherwise subsequent chunks carry the marshaled SBOMSyft bytes. + GetSBOM(ctx context.Context, in *GetSBOMRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GetSBOMChunk], error) } type storageServiceClient struct { @@ -116,19 +113,9 @@ func (c *storageServiceClient) ListNetworkNeighborhoods(ctx context.Context, in return out, nil } -func (c *storageServiceClient) PutSBOM(ctx context.Context, in *PutSBOMRequest, opts ...grpc.CallOption) (*PutSBOMResponse, error) { +func (c *storageServiceClient) PutSBOM(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[PutSBOMChunk, PutSBOMResponse], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(PutSBOMResponse) - err := c.cc.Invoke(ctx, StorageService_PutSBOM_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *storageServiceClient) PutSBOMStream(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[PutSBOMChunk, PutSBOMResponse], error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &StorageService_ServiceDesc.Streams[0], StorageService_PutSBOMStream_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &StorageService_ServiceDesc.Streams[0], StorageService_PutSBOM_FullMethodName, cOpts...) if err != nil { return nil, err } @@ -137,21 +124,11 @@ func (c *storageServiceClient) PutSBOMStream(ctx context.Context, opts ...grpc.C } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type StorageService_PutSBOMStreamClient = grpc.ClientStreamingClient[PutSBOMChunk, PutSBOMResponse] +type StorageService_PutSBOMClient = grpc.ClientStreamingClient[PutSBOMChunk, PutSBOMResponse] -func (c *storageServiceClient) GetSBOM(ctx context.Context, in *GetSBOMRequest, opts ...grpc.CallOption) (*GetSBOMResponse, error) { +func (c *storageServiceClient) GetSBOM(ctx context.Context, in *GetSBOMRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GetSBOMChunk], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - out := new(GetSBOMResponse) - err := c.cc.Invoke(ctx, StorageService_GetSBOM_FullMethodName, in, out, cOpts...) - if err != nil { - return nil, err - } - return out, nil -} - -func (c *storageServiceClient) GetSBOMStream(ctx context.Context, in *GetSBOMRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GetSBOMChunk], error) { - cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &StorageService_ServiceDesc.Streams[1], StorageService_GetSBOMStream_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &StorageService_ServiceDesc.Streams[1], StorageService_GetSBOM_FullMethodName, cOpts...) if err != nil { return nil, err } @@ -166,7 +143,7 @@ func (c *storageServiceClient) GetSBOMStream(ctx context.Context, in *GetSBOMReq } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type StorageService_GetSBOMStreamClient = grpc.ServerStreamingClient[GetSBOMChunk] +type StorageService_GetSBOMClient = grpc.ServerStreamingClient[GetSBOMChunk] // StorageServiceServer is the server API for StorageService service. // All implementations must embed UnimplementedStorageServiceServer @@ -184,27 +161,26 @@ type StorageServiceServer interface { ListApplicationProfiles(context.Context, *ListApplicationProfilesRequest) (*ListApplicationProfilesResponse, error) // ListNetworkNeighborhoods lists all NetworkNeighborhoods in a namespace (returns metadata only, nil Spec) ListNetworkNeighborhoods(context.Context, *ListNetworkNeighborhoodsRequest) (*ListNetworkNeighborhoodsResponse, error) - // PutSBOM uploads an SBOM produced for an image. Idempotent on - // (customer_guid, image_digest, syft_version) — duplicate calls upsert the - // metadata row and overwrite the S3 blob; end-state is consistent. Used - // for SBOM payloads up to the default 4 MiB gRPC message limit. - PutSBOM(context.Context, *PutSBOMRequest) (*PutSBOMResponse, error) - // PutSBOMStream uploads an SBOM larger than the default 4 MiB unary gRPC - // message limit, chunked client-side. The first chunk in the stream MUST - // set metadata (image_digest, syft_version, source); subsequent chunks - // set only blob_chunk. Concatenated chunks form the serialized SBOMSyft - // proto payload. - PutSBOMStream(grpc.ClientStreamingServer[PutSBOMChunk, PutSBOMResponse]) error + // PutSBOM uploads an SBOM produced for an image. Client-streaming so the + // caller need not know the payload size in advance and can stream bytes + // from any io.Reader. Idempotent on (customer_guid, image_digest, + // syft_version) — duplicate calls upsert the metadata row and overwrite + // the S3 blob; end-state is consistent. + // + // The first chunk MUST set metadata (image_digest, syft_version, source) + // and MAY include blob_chunk for small payloads. Subsequent chunks set + // only blob_chunk. Concatenated blob_chunk values form the marshaled + // SBOMSyft proto payload. + PutSBOM(grpc.ClientStreamingServer[PutSBOMChunk, PutSBOMResponse]) error // GetSBOM probes for or fetches an SBOM by (customer_guid, image_digest, - // syft_version). Set metadata_only=true to skip transferring the blob — - // used by agents to decide whether to regenerate. Returns the same - // envelope (with exists=false) on a miss. - GetSBOM(context.Context, *GetSBOMRequest) (*GetSBOMResponse, error) - // GetSBOMStream returns an SBOM larger than the default 4 MiB unary gRPC - // message limit, server-streamed. The first chunk sets metadata + status; - // subsequent chunks set only blob_chunk. The metadata_only flag on the - // request is ignored — GetSBOM (unary) is the metadata-only entry point. - GetSBOMStream(*GetSBOMRequest, grpc.ServerStreamingServer[GetSBOMChunk]) error + // syft_version). Server-streaming so the caller need not know the + // response size in advance. + // + // The first chunk in the response carries metadata + status. When the + // request sets metadata_only=true OR the row does not exist OR success + // is false, the server closes the stream after the metadata chunk. + // Otherwise subsequent chunks carry the marshaled SBOMSyft bytes. + GetSBOM(*GetSBOMRequest, grpc.ServerStreamingServer[GetSBOMChunk]) error mustEmbedUnimplementedStorageServiceServer() } @@ -227,17 +203,11 @@ func (UnimplementedStorageServiceServer) ListApplicationProfiles(context.Context func (UnimplementedStorageServiceServer) ListNetworkNeighborhoods(context.Context, *ListNetworkNeighborhoodsRequest) (*ListNetworkNeighborhoodsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListNetworkNeighborhoods not implemented") } -func (UnimplementedStorageServiceServer) PutSBOM(context.Context, *PutSBOMRequest) (*PutSBOMResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method PutSBOM not implemented") -} -func (UnimplementedStorageServiceServer) PutSBOMStream(grpc.ClientStreamingServer[PutSBOMChunk, PutSBOMResponse]) error { - return status.Errorf(codes.Unimplemented, "method PutSBOMStream not implemented") +func (UnimplementedStorageServiceServer) PutSBOM(grpc.ClientStreamingServer[PutSBOMChunk, PutSBOMResponse]) error { + return status.Errorf(codes.Unimplemented, "method PutSBOM not implemented") } -func (UnimplementedStorageServiceServer) GetSBOM(context.Context, *GetSBOMRequest) (*GetSBOMResponse, error) { - return nil, status.Errorf(codes.Unimplemented, "method GetSBOM not implemented") -} -func (UnimplementedStorageServiceServer) GetSBOMStream(*GetSBOMRequest, grpc.ServerStreamingServer[GetSBOMChunk]) error { - return status.Errorf(codes.Unimplemented, "method GetSBOMStream not implemented") +func (UnimplementedStorageServiceServer) GetSBOM(*GetSBOMRequest, grpc.ServerStreamingServer[GetSBOMChunk]) error { + return status.Errorf(codes.Unimplemented, "method GetSBOM not implemented") } func (UnimplementedStorageServiceServer) mustEmbedUnimplementedStorageServiceServer() {} func (UnimplementedStorageServiceServer) testEmbeddedByValue() {} @@ -332,59 +302,23 @@ func _StorageService_ListNetworkNeighborhoods_Handler(srv interface{}, ctx conte return interceptor(ctx, in, info, handler) } -func _StorageService_PutSBOM_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(PutSBOMRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(StorageServiceServer).PutSBOM(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: StorageService_PutSBOM_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(StorageServiceServer).PutSBOM(ctx, req.(*PutSBOMRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _StorageService_PutSBOMStream_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(StorageServiceServer).PutSBOMStream(&grpc.GenericServerStream[PutSBOMChunk, PutSBOMResponse]{ServerStream: stream}) +func _StorageService_PutSBOM_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(StorageServiceServer).PutSBOM(&grpc.GenericServerStream[PutSBOMChunk, PutSBOMResponse]{ServerStream: stream}) } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type StorageService_PutSBOMStreamServer = grpc.ClientStreamingServer[PutSBOMChunk, PutSBOMResponse] +type StorageService_PutSBOMServer = grpc.ClientStreamingServer[PutSBOMChunk, PutSBOMResponse] -func _StorageService_GetSBOM_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(GetSBOMRequest) - if err := dec(in); err != nil { - return nil, err - } - if interceptor == nil { - return srv.(StorageServiceServer).GetSBOM(ctx, in) - } - info := &grpc.UnaryServerInfo{ - Server: srv, - FullMethod: StorageService_GetSBOM_FullMethodName, - } - handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(StorageServiceServer).GetSBOM(ctx, req.(*GetSBOMRequest)) - } - return interceptor(ctx, in, info, handler) -} - -func _StorageService_GetSBOMStream_Handler(srv interface{}, stream grpc.ServerStream) error { +func _StorageService_GetSBOM_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(GetSBOMRequest) if err := stream.RecvMsg(m); err != nil { return err } - return srv.(StorageServiceServer).GetSBOMStream(m, &grpc.GenericServerStream[GetSBOMRequest, GetSBOMChunk]{ServerStream: stream}) + return srv.(StorageServiceServer).GetSBOM(m, &grpc.GenericServerStream[GetSBOMRequest, GetSBOMChunk]{ServerStream: stream}) } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type StorageService_GetSBOMStreamServer = grpc.ServerStreamingServer[GetSBOMChunk] +type StorageService_GetSBOMServer = grpc.ServerStreamingServer[GetSBOMChunk] // StorageService_ServiceDesc is the grpc.ServiceDesc for StorageService service. // It's only intended for direct use with grpc.RegisterService, @@ -409,24 +343,16 @@ var StorageService_ServiceDesc = grpc.ServiceDesc{ MethodName: "ListNetworkNeighborhoods", Handler: _StorageService_ListNetworkNeighborhoods_Handler, }, - { - MethodName: "PutSBOM", - Handler: _StorageService_PutSBOM_Handler, - }, - { - MethodName: "GetSBOM", - Handler: _StorageService_GetSBOM_Handler, - }, }, Streams: []grpc.StreamDesc{ { - StreamName: "PutSBOMStream", - Handler: _StorageService_PutSBOMStream_Handler, + StreamName: "PutSBOM", + Handler: _StorageService_PutSBOM_Handler, ClientStreams: true, }, { - StreamName: "GetSBOMStream", - Handler: _StorageService_GetSBOMStream_Handler, + StreamName: "GetSBOM", + Handler: _StorageService_GetSBOM_Handler, ServerStreams: true, }, }, diff --git a/pkg/client/v1/storageclient.go b/pkg/client/v1/storageclient.go index 08a714a..74c28a4 100644 --- a/pkg/client/v1/storageclient.go +++ b/pkg/client/v1/storageclient.go @@ -1,6 +1,7 @@ package v1 import ( + "bytes" "context" "crypto/tls" "fmt" @@ -493,19 +494,19 @@ func (c *StorageClient) ListNetworkNeighborhoods(ctx context.Context, namespace return list, nil } -// PutSBOM uploads an SBOM to the storage server. Use for payloads small -// enough to fit in a single gRPC message (~4 MiB). For larger SBOMs, use -// PutSBOMStream instead. -func (c *StorageClient) PutSBOM(ctx context.Context, imageDigest, syftVersion string, source proto.SBOMSource, sbom *v1beta1.SBOMSyft) (*proto.PutSBOMResponse, error) { +// PutSBOM uploads an SBOM identified by (image_digest, syft_version, +// source). The payload is the marshaled SBOMSyft proto, read from r and +// sent to the server in chunks of sbomStreamChunkSize. Callers with a +// typed *v1beta1.SBOMSyft should use MarshalSBOM to obtain r. +// +// The underlying RPC is client-streaming; the caller never needs to know +// the payload size in advance. +func (c *StorageClient) PutSBOM(ctx context.Context, imageDigest, syftVersion string, source proto.SBOMSource, r io.Reader) (*proto.PutSBOMResponse, error) { if c.protoClient == nil { return nil, fmt.Errorf("client is not connected") } - - req := &proto.PutSBOMRequest{ - ImageDigest: imageDigest, - SyftVersion: syftVersion, - Source: source, - Sbom: sbom, + if r == nil { + return nil, fmt.Errorf("payload reader is nil") } ctx = c.withMetadata(ctx) @@ -516,79 +517,63 @@ func (c *StorageClient) PutSBOM(ctx context.Context, imageDigest, syftVersion st defer cancel() } - return c.protoClient.PutSBOM(ctx, req) -} - -// PutSBOMStream uploads an SBOM to the storage server in chunks. The SBOM -// is marshaled to its proto wire form and split into ~1 MiB chunks; the -// first chunk carries metadata, subsequent chunks carry blob bytes only. -// Use when the SBOM is too large for unary PutSBOM (~4 MiB default gRPC -// message limit). -func (c *StorageClient) PutSBOMStream(ctx context.Context, imageDigest, syftVersion string, source proto.SBOMSource, sbom *v1beta1.SBOMSyft) (*proto.PutSBOMResponse, error) { - if c.protoClient == nil { - return nil, fmt.Errorf("client is not connected") - } - - if sbom == nil { - return nil, fmt.Errorf("sbom is nil") - } - - payload, err := sbom.Marshal() + stream, err := c.protoClient.PutSBOM(ctx) if err != nil { - return nil, fmt.Errorf("failed to marshal SBOMSyft: %w", err) - } - - ctx = c.withMetadata(ctx) - - if c.callTimeout != nil && *c.callTimeout > 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, *c.callTimeout) - defer cancel() + return nil, fmt.Errorf("failed to open PutSBOM stream: %w", err) } - stream, err := c.protoClient.PutSBOMStream(ctx) - if err != nil { - return nil, fmt.Errorf("failed to open PutSBOMStream: %w", err) + // First chunk MUST set metadata, and MAY carry the first slice of bytes. + buf := make([]byte, sbomStreamChunkSize) + n, readErr := io.ReadFull(r, buf) + if readErr != nil && readErr != io.EOF && readErr != io.ErrUnexpectedEOF { + return nil, fmt.Errorf("failed to read payload: %w", readErr) } - - // First chunk carries metadata + (optionally) the first slice of bytes. first := &proto.PutSBOMChunk{ Metadata: &proto.PutSBOMChunkMetadata{ ImageDigest: imageDigest, SyftVersion: syftVersion, Source: source, }, + BlobChunk: buf[:n], } - cut := sbomStreamChunkSize - if cut > len(payload) { - cut = len(payload) - } - first.BlobChunk = payload[:cut] if err := stream.Send(first); err != nil { return nil, fmt.Errorf("failed to send first chunk: %w", err) } - // Subsequent chunks carry blob bytes only. - for offset := cut; offset < len(payload); offset += sbomStreamChunkSize { - end := offset + sbomStreamChunkSize - if end > len(payload) { - end = len(payload) - } - if err := stream.Send(&proto.PutSBOMChunk{BlobChunk: payload[offset:end]}); err != nil { - return nil, fmt.Errorf("failed to send chunk: %w", err) + // Subsequent chunks carry blob bytes only. ReadFull returns + // io.ErrUnexpectedEOF on a short final read, which is normal — we send + // whatever bytes we got and then stop on the next iteration. + for readErr == nil { + n, readErr = io.ReadFull(r, buf) + if n > 0 { + if err := stream.Send(&proto.PutSBOMChunk{BlobChunk: buf[:n]}); err != nil { + return nil, fmt.Errorf("failed to send chunk: %w", err) + } } } + if readErr != io.EOF && readErr != io.ErrUnexpectedEOF { + return nil, fmt.Errorf("failed to read payload: %w", readErr) + } return stream.CloseAndRecv() } // GetSBOM probes for or fetches an SBOM by (image_digest, syft_version). -// Set metadataOnly=true to skip the blob and only check whether an SBOM -// exists. For SBOMs too large to fit in a single gRPC message, use -// GetSBOMStream when metadataOnly is false. -func (c *StorageClient) GetSBOM(ctx context.Context, imageDigest, syftVersion string, metadataOnly bool) (*proto.GetSBOMResponse, error) { +// +// When metadataOnly is true, or the row does not exist, or the server +// reports a non-success status, the returned io.ReadCloser is nil — the +// caller consults the metadata for the answer. +// +// Otherwise the returned io.ReadCloser streams the marshaled SBOMSyft +// proto bytes; use UnmarshalSBOM (or read into your own buffer) to +// reconstruct the typed object. The caller MUST Close the reader to +// release the underlying gRPC stream. +// +// The underlying RPC is server-streaming; the caller never needs to know +// the response size in advance. +func (c *StorageClient) GetSBOM(ctx context.Context, imageDigest, syftVersion string, metadataOnly bool) (*proto.GetSBOMChunkMetadata, io.ReadCloser, error) { if c.protoClient == nil { - return nil, fmt.Errorf("client is not connected") + return nil, nil, fmt.Errorf("client is not connected") } req := &proto.GetSBOMRequest{ @@ -599,79 +584,111 @@ func (c *StorageClient) GetSBOM(ctx context.Context, imageDigest, syftVersion st ctx = c.withMetadata(ctx) - if c.callTimeout != nil && *c.callTimeout > 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, *c.callTimeout) - defer cancel() - } - - return c.protoClient.GetSBOM(ctx, req) -} - -// GetSBOMStream fetches an SBOM in chunks. The first stream chunk returns -// metadata + status; subsequent chunks carry the marshaled SBOMSyft bytes, -// which are concatenated and unmarshaled by this method. Returns -// (metadata, nil) when the server reports exists=false or success=false — -// the caller should consult metadata.Success / metadata.Exists before -// using the returned SBOMSyft. -func (c *StorageClient) GetSBOMStream(ctx context.Context, imageDigest, syftVersion string) (*proto.GetSBOMChunkMetadata, *v1beta1.SBOMSyft, error) { - if c.protoClient == nil { - return nil, nil, fmt.Errorf("client is not connected") - } - - req := &proto.GetSBOMRequest{ - ImageDigest: imageDigest, - SyftVersion: syftVersion, - } - - ctx = c.withMetadata(ctx) - - if c.callTimeout != nil && *c.callTimeout > 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, *c.callTimeout) - defer cancel() - } - - stream, err := c.protoClient.GetSBOMStream(ctx, req) + // Note: we deliberately do NOT apply callTimeout here, because the + // caller drives the read cadence via the returned io.ReadCloser. A + // per-call timeout would clip large downloads. Callers wanting a + // timeout should pass a ctx with their own deadline. + streamCtx, cancel := context.WithCancel(ctx) + stream, err := c.protoClient.GetSBOM(streamCtx, req) if err != nil { - return nil, nil, fmt.Errorf("failed to open GetSBOMStream: %w", err) + cancel() + return nil, nil, fmt.Errorf("failed to open GetSBOM stream: %w", err) } // First chunk MUST carry metadata. firstChunk, err := stream.Recv() if err != nil { + cancel() return nil, nil, fmt.Errorf("failed to receive first chunk: %w", err) } md := firstChunk.GetMetadata() if md == nil { - return nil, nil, fmt.Errorf("first GetSBOMStream chunk missing metadata") + cancel() + return nil, nil, fmt.Errorf("first GetSBOM chunk missing metadata") } - // On error or miss the server closes the stream after the metadata chunk. - if !md.Success || !md.Exists { + // On miss, error, or metadata-only request, the server closes the + // stream after the metadata chunk. Drain and return nil reader. + if !md.Success || !md.Exists || metadataOnly { + cancel() return md, nil, nil } - // Accumulate blob bytes. The first chunk may have carried some payload - // alongside the metadata. - var buf []byte - if len(firstChunk.BlobChunk) > 0 { - buf = append(buf, firstChunk.BlobChunk...) + reader := &sbomStreamReader{ + stream: stream, + pending: firstChunk.BlobChunk, + cancel: cancel, } - for { - chunk, err := stream.Recv() + return md, reader, nil +} + +// sbomStreamReader adapts a server-streaming GetSBOM RPC to an +// io.ReadCloser, lazily fetching the next chunk when the previous one is +// drained. +type sbomStreamReader struct { + stream grpc.ServerStreamingClient[proto.GetSBOMChunk] + pending []byte // buffer of bytes from the most-recent chunk not yet read + cancel context.CancelFunc + err error // sticky error; once set, Read returns it on every call +} + +func (r *sbomStreamReader) Read(p []byte) (int, error) { + if r.err != nil { + return 0, r.err + } + for len(r.pending) == 0 { + chunk, err := r.stream.Recv() if err == io.EOF { - break + r.err = io.EOF + return 0, io.EOF } if err != nil { - return md, nil, fmt.Errorf("failed to receive chunk: %w", err) + r.err = err + return 0, err } - buf = append(buf, chunk.BlobChunk...) + r.pending = chunk.BlobChunk } + n := copy(p, r.pending) + r.pending = r.pending[n:] + return n, nil +} +func (r *sbomStreamReader) Close() error { + if r.cancel != nil { + r.cancel() + r.cancel = nil + } + return nil +} + +// MarshalSBOM marshals a typed SBOMSyft to its proto wire bytes and +// returns a reader over them, ready for PutSBOM. The full marshaled blob +// is held in memory; for very large SBOMs callers may prefer to write +// pre-marshaled bytes to a temp file and pass an *os.File to PutSBOM. +func MarshalSBOM(sbom *v1beta1.SBOMSyft) (io.Reader, error) { + if sbom == nil { + return nil, fmt.Errorf("sbom is nil") + } + b, err := sbom.Marshal() + if err != nil { + return nil, fmt.Errorf("failed to marshal SBOMSyft: %w", err) + } + return bytes.NewReader(b), nil +} + +// UnmarshalSBOM reads marshaled SBOMSyft proto bytes from r and returns +// the decoded object. It reads r until EOF; it does not Close r. +func UnmarshalSBOM(r io.Reader) (*v1beta1.SBOMSyft, error) { + if r == nil { + return nil, fmt.Errorf("reader is nil") + } + b, err := io.ReadAll(r) + if err != nil { + return nil, fmt.Errorf("failed to read SBOM bytes: %w", err) + } sbom := &v1beta1.SBOMSyft{} - if err := sbom.Unmarshal(buf); err != nil { - return md, nil, fmt.Errorf("failed to unmarshal SBOMSyft: %w", err) + if err := sbom.Unmarshal(b); err != nil { + return nil, fmt.Errorf("failed to unmarshal SBOMSyft: %w", err) } - return md, sbom, nil + return sbom, nil } diff --git a/pkg/client/v1/storageclient_test.go b/pkg/client/v1/storageclient_test.go index 26a5ea2..43dcb47 100644 --- a/pkg/client/v1/storageclient_test.go +++ b/pkg/client/v1/storageclient_test.go @@ -3,6 +3,9 @@ package v1 import ( "context" "fmt" + "io" + "net" + "sync" "testing" "time" @@ -12,6 +15,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/test/bufconn" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // Mock StorageServiceClient for testing @@ -20,10 +26,8 @@ type mockStorageServiceClient struct { getProfileFunc func(ctx context.Context, in *proto.GetProfileRequest, opts ...grpc.CallOption) (*proto.GetProfileResponse, error) listApplicationProfilesFunc func(ctx context.Context, in *proto.ListApplicationProfilesRequest, opts ...grpc.CallOption) (*proto.ListApplicationProfilesResponse, error) listNetworkNeighborhoodsFunc func(ctx context.Context, in *proto.ListNetworkNeighborhoodsRequest, opts ...grpc.CallOption) (*proto.ListNetworkNeighborhoodsResponse, error) - putSBOMFunc func(ctx context.Context, in *proto.PutSBOMRequest, opts ...grpc.CallOption) (*proto.PutSBOMResponse, error) - getSBOMFunc func(ctx context.Context, in *proto.GetSBOMRequest, opts ...grpc.CallOption) (*proto.GetSBOMResponse, error) - putSBOMStreamFunc func(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[proto.PutSBOMChunk, proto.PutSBOMResponse], error) - getSBOMStreamFunc func(ctx context.Context, in *proto.GetSBOMRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[proto.GetSBOMChunk], error) + putSBOMFunc func(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[proto.PutSBOMChunk, proto.PutSBOMResponse], error) + getSBOMFunc func(ctx context.Context, in *proto.GetSBOMRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[proto.GetSBOMChunk], error) } func (m *mockStorageServiceClient) SendContainerProfile(ctx context.Context, in *proto.SendContainerProfileRequest, opts ...grpc.CallOption) (*proto.SendContainerProfileResponse, error) { @@ -54,32 +58,18 @@ func (m *mockStorageServiceClient) ListNetworkNeighborhoods(ctx context.Context, return &proto.ListNetworkNeighborhoodsResponse{Success: true}, nil } -func (m *mockStorageServiceClient) PutSBOM(ctx context.Context, in *proto.PutSBOMRequest, opts ...grpc.CallOption) (*proto.PutSBOMResponse, error) { +func (m *mockStorageServiceClient) PutSBOM(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[proto.PutSBOMChunk, proto.PutSBOMResponse], error) { if m.putSBOMFunc != nil { - return m.putSBOMFunc(ctx, in, opts...) + return m.putSBOMFunc(ctx, opts...) } - return &proto.PutSBOMResponse{Success: true}, nil + return nil, fmt.Errorf("PutSBOM not implemented in mock") } -func (m *mockStorageServiceClient) GetSBOM(ctx context.Context, in *proto.GetSBOMRequest, opts ...grpc.CallOption) (*proto.GetSBOMResponse, error) { +func (m *mockStorageServiceClient) GetSBOM(ctx context.Context, in *proto.GetSBOMRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[proto.GetSBOMChunk], error) { if m.getSBOMFunc != nil { return m.getSBOMFunc(ctx, in, opts...) } - return &proto.GetSBOMResponse{Success: true}, nil -} - -func (m *mockStorageServiceClient) PutSBOMStream(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[proto.PutSBOMChunk, proto.PutSBOMResponse], error) { - if m.putSBOMStreamFunc != nil { - return m.putSBOMStreamFunc(ctx, opts...) - } - return nil, fmt.Errorf("PutSBOMStream not implemented in mock") -} - -func (m *mockStorageServiceClient) GetSBOMStream(ctx context.Context, in *proto.GetSBOMRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[proto.GetSBOMChunk], error) { - if m.getSBOMStreamFunc != nil { - return m.getSBOMStreamFunc(ctx, in, opts...) - } - return nil, fmt.Errorf("GetSBOMStream not implemented in mock") + return nil, fmt.Errorf("GetSBOM not implemented in mock") } func TestNewStorageClient(t *testing.T) { @@ -649,99 +639,307 @@ func TestParseGRPCURL(t *testing.T) { } } -func TestStorageClient_PutSBOM(t *testing.T) { - client, err := NewStorageClient("grpc://storage.example.com:50051", "test-account", "test-key", "test-cluster") - require.NoError(t, err) +// sbomRoundTripServer is a minimal proto.StorageServiceServer impl that +// records what PutSBOM receives and serves what GetSBOM should return. +// It exercises the full marshal/unmarshal path of the streaming RPCs +// end-to-end through bufconn — catching wire-shape bugs the prior +// mock-based tests would miss. +type sbomRoundTripServer struct { + proto.UnimplementedStorageServiceServer + mu sync.Mutex + + // Received state from the most recent PutSBOM call. + receivedMetadata *proto.PutSBOMChunkMetadata + receivedBytes []byte + + // Configured response for GetSBOM. If exists is false the server + // closes the stream after the metadata chunk. + serveExists bool + serveMetadata *proto.SBOMMetadata + serveBytes []byte + serveChunkSize int // 0 → send the whole payload in one chunk +} - const ( - imageDigest = "abc123def456" - syftVersion = "1.0.0" - ) +func (s *sbomRoundTripServer) PutSBOM(stream grpc.ClientStreamingServer[proto.PutSBOMChunk, proto.PutSBOMResponse]) error { + var buf []byte + for { + chunk, err := stream.Recv() + if err == io.EOF { + break + } + if err != nil { + return err + } + s.mu.Lock() + if chunk.Metadata != nil && s.receivedMetadata == nil { + s.receivedMetadata = chunk.Metadata + } + s.mu.Unlock() + buf = append(buf, chunk.BlobChunk...) + } + s.mu.Lock() + s.receivedBytes = buf + s.mu.Unlock() + return stream.SendAndClose(&proto.PutSBOMResponse{Success: true}) +} - mockClient := &mockStorageServiceClient{ - putSBOMFunc: func(ctx context.Context, in *proto.PutSBOMRequest, opts ...grpc.CallOption) (*proto.PutSBOMResponse, error) { - assert.Equal(t, imageDigest, in.ImageDigest) - assert.Equal(t, syftVersion, in.SyftVersion) - assert.Equal(t, proto.SBOMSource_SBOM_SOURCE_WORKLOAD, in.Source) - assert.NotNil(t, in.Sbom) - return &proto.PutSBOMResponse{Success: true}, nil +func (s *sbomRoundTripServer) GetSBOM(req *proto.GetSBOMRequest, stream grpc.ServerStreamingServer[proto.GetSBOMChunk]) error { + s.mu.Lock() + exists := s.serveExists + metadata := s.serveMetadata + payload := s.serveBytes + chunkSize := s.serveChunkSize + s.mu.Unlock() + + // First chunk MUST carry the metadata header. + first := &proto.GetSBOMChunk{ + Metadata: &proto.GetSBOMChunkMetadata{ + Success: true, + Exists: exists, + SbomMetadata: metadata, }, } - client.protoClient = mockClient + if err := stream.Send(first); err != nil { + return err + } - resp, err := client.PutSBOM(context.Background(), imageDigest, syftVersion, proto.SBOMSource_SBOM_SOURCE_WORKLOAD, &v1beta1.SBOMSyft{}) - require.NoError(t, err) - assert.True(t, resp.Success) + if !exists || req.MetadataOnly { + return nil + } + + if chunkSize <= 0 || chunkSize >= len(payload) { + return stream.Send(&proto.GetSBOMChunk{BlobChunk: payload}) + } + for offset := 0; offset < len(payload); offset += chunkSize { + end := offset + chunkSize + if end > len(payload) { + end = len(payload) + } + if err := stream.Send(&proto.GetSBOMChunk{BlobChunk: payload[offset:end]}); err != nil { + return err + } + } + return nil } -func TestStorageClient_GetSBOM(t *testing.T) { - client, err := NewStorageClient("grpc://storage.example.com:50051", "test-account", "test-key", "test-cluster") +// startBufconnSBOMServer starts the round-trip server on an in-memory +// bufconn listener and returns a client connected to it. +func startBufconnSBOMServer(t *testing.T) (*sbomRoundTripServer, *StorageClient, func()) { + t.Helper() + const bufsize = 1024 * 1024 + lis := bufconn.Listen(bufsize) + srv := grpc.NewServer() + rtSrv := &sbomRoundTripServer{} + proto.RegisterStorageServiceServer(srv, rtSrv) + go func() { _ = srv.Serve(lis) }() + + conn, err := grpc.NewClient( + "passthrough:///bufnet", + grpc.WithContextDialer(func(_ context.Context, _ string) (net.Conn, error) { return lis.Dial() }), + grpc.WithTransportCredentials(insecure.NewCredentials()), + ) require.NoError(t, err) - tests := []struct { - name string - imageDigest string - syftVersion string - metadataOnly bool - exists bool - }{ - { - name: "metadata-only probe, hit", - imageDigest: "abc123", - syftVersion: "1.0.0", - metadataOnly: true, - exists: true, + client, err := NewStorageClient("grpc://example.com:50051", "test-account", "test-key", "test-cluster") + require.NoError(t, err) + client.conn = conn + client.protoClient = proto.NewStorageServiceClient(conn) + + cleanup := func() { + _ = conn.Close() + srv.Stop() + } + return rtSrv, client, cleanup +} + +// sampleSBOMSyft constructs a non-trivial SBOMSyft for round-trip tests +// so the proto Marshal / Unmarshal path actually has fields to round-trip. +func sampleSBOMSyft() *v1beta1.SBOMSyft { + return &v1beta1.SBOMSyft{ + TypeMeta: metav1.TypeMeta{ + Kind: "SBOMSyft", + APIVersion: "spdx.softwarecomposition.kubescape.io/v1beta1", }, - { - name: "metadata-only probe, miss", - imageDigest: "deadbeef", - syftVersion: "1.0.0", - metadataOnly: true, - exists: false, + ObjectMeta: metav1.ObjectMeta{ + Name: "sha256-abc123", + Namespace: "kubescape", + Annotations: map[string]string{ + "image.name": "library/nginx:latest", + "syft.version": "1.0.0", + }, }, - { - name: "full fetch, hit", - imageDigest: "abc123", - syftVersion: "1.0.0", - metadataOnly: false, - exists: true, + Spec: v1beta1.SBOMSyftSpec{ + Metadata: v1beta1.SPDXMeta{ + Tool: v1beta1.ToolMeta{ + Name: "syft", + Version: "1.0.0", + }, + }, + Syft: v1beta1.SyftDocument{ + SyftSource: v1beta1.SyftSource{Type: "image"}, + }, }, } +} - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - mockClient := &mockStorageServiceClient{ - getSBOMFunc: func(ctx context.Context, in *proto.GetSBOMRequest, opts ...grpc.CallOption) (*proto.GetSBOMResponse, error) { - assert.Equal(t, tc.imageDigest, in.ImageDigest) - assert.Equal(t, tc.syftVersion, in.SyftVersion) - assert.Equal(t, tc.metadataOnly, in.MetadataOnly) - resp := &proto.GetSBOMResponse{Success: true, Exists: tc.exists} - if tc.exists { - resp.Metadata = &proto.SBOMMetadata{ - ImageDigest: tc.imageDigest, - SyftVersion: tc.syftVersion, - } - if !tc.metadataOnly { - resp.Sbom = &v1beta1.SBOMSyft{} - } - } - return resp, nil - }, - } - client.protoClient = mockClient +// TestStorageClient_SBOMRoundTrip is the end-to-end marshal/unmarshal test +// matthyx asked for: it stands up a real gRPC server over bufconn, ships +// a non-trivial SBOMSyft via the client's PutSBOM, then pulls it back via +// GetSBOM, and verifies semantic equality at every hop. Covers four +// shapes: +// - Probe (metadata_only=true), exists path +// - Probe (metadata_only=true), miss path +// - Full fetch, payload fits in one chunk +// - Full fetch, payload is split across many small chunks (forces the +// reader to drain across multiple Recv calls) +func TestStorageClient_SBOMRoundTrip(t *testing.T) { + const ( + imageDigest = "abc123def456" + syftVersion = "1.0.0" + ) + source := proto.SBOMSource_SBOM_SOURCE_WORKLOAD + + t.Run("PutSBOM marshals through MarshalSBOM and arrives intact", func(t *testing.T) { + rtSrv, client, cleanup := startBufconnSBOMServer(t) + defer cleanup() + + original := sampleSBOMSyft() + reader, err := MarshalSBOM(original) + require.NoError(t, err) + + resp, err := client.PutSBOM(context.Background(), imageDigest, syftVersion, source, reader) + require.NoError(t, err) + assert.True(t, resp.Success) + + // Server received the right metadata header. + require.NotNil(t, rtSrv.receivedMetadata) + assert.Equal(t, imageDigest, rtSrv.receivedMetadata.ImageDigest) + assert.Equal(t, syftVersion, rtSrv.receivedMetadata.SyftVersion) + assert.Equal(t, source, rtSrv.receivedMetadata.Source) + + // Server-received bytes unmarshal to a SBOMSyft equal to the original. + require.NotEmpty(t, rtSrv.receivedBytes) + got := &v1beta1.SBOMSyft{} + require.NoError(t, got.Unmarshal(rtSrv.receivedBytes)) + assert.Equal(t, original.Name, got.Name) + assert.Equal(t, original.Namespace, got.Namespace) + assert.Equal(t, original.Spec.Metadata.Tool.Name, got.Spec.Metadata.Tool.Name) + assert.Equal(t, original.Spec.Metadata.Tool.Version, got.Spec.Metadata.Tool.Version) + assert.Equal(t, original.Annotations["image.name"], got.Annotations["image.name"]) + }) - resp, err := client.GetSBOM(context.Background(), tc.imageDigest, tc.syftVersion, tc.metadataOnly) - require.NoError(t, err) - assert.True(t, resp.Success) - assert.Equal(t, tc.exists, resp.Exists) - if tc.exists { - assert.NotNil(t, resp.Metadata) - if tc.metadataOnly { - assert.Nil(t, resp.Sbom) - } else { - assert.NotNil(t, resp.Sbom) - } + t.Run("GetSBOM metadata-only probe returns only metadata", func(t *testing.T) { + rtSrv, client, cleanup := startBufconnSBOMServer(t) + defer cleanup() + + rtSrv.serveExists = true + rtSrv.serveMetadata = &proto.SBOMMetadata{ + ImageDigest: imageDigest, + SyftVersion: syftVersion, + } + + md, reader, err := client.GetSBOM(context.Background(), imageDigest, syftVersion, true) + require.NoError(t, err) + assert.True(t, md.Success) + assert.True(t, md.Exists) + require.NotNil(t, md.SbomMetadata) + assert.Equal(t, imageDigest, md.SbomMetadata.ImageDigest) + assert.Nil(t, reader, "metadata-only probe must not return a reader") + }) + + t.Run("GetSBOM probe miss returns exists=false and no reader", func(t *testing.T) { + rtSrv, client, cleanup := startBufconnSBOMServer(t) + defer cleanup() + + rtSrv.serveExists = false + + md, reader, err := client.GetSBOM(context.Background(), imageDigest, syftVersion, true) + require.NoError(t, err) + assert.True(t, md.Success) + assert.False(t, md.Exists) + assert.Nil(t, reader) + }) + + t.Run("GetSBOM full fetch round-trips through UnmarshalSBOM (single chunk)", func(t *testing.T) { + rtSrv, client, cleanup := startBufconnSBOMServer(t) + defer cleanup() + + original := sampleSBOMSyft() + payload, err := original.Marshal() + require.NoError(t, err) + rtSrv.serveExists = true + rtSrv.serveMetadata = &proto.SBOMMetadata{ImageDigest: imageDigest, SyftVersion: syftVersion} + rtSrv.serveBytes = payload + rtSrv.serveChunkSize = 0 // single chunk + + md, reader, err := client.GetSBOM(context.Background(), imageDigest, syftVersion, false) + require.NoError(t, err) + require.NotNil(t, reader) + defer reader.Close() + assert.True(t, md.Exists) + + got, err := UnmarshalSBOM(reader) + require.NoError(t, err) + assert.Equal(t, original.Name, got.Name) + assert.Equal(t, original.Spec.Metadata.Tool.Name, got.Spec.Metadata.Tool.Name) + }) + + t.Run("GetSBOM full fetch round-trips with payload split across many chunks", func(t *testing.T) { + rtSrv, client, cleanup := startBufconnSBOMServer(t) + defer cleanup() + + original := sampleSBOMSyft() + payload, err := original.Marshal() + require.NoError(t, err) + rtSrv.serveExists = true + rtSrv.serveMetadata = &proto.SBOMMetadata{ImageDigest: imageDigest, SyftVersion: syftVersion} + rtSrv.serveBytes = payload + // 7-byte chunks force the reader to drain over many Recv calls and + // across reads smaller than a chunk — exercises the buffering logic. + rtSrv.serveChunkSize = 7 + + md, reader, err := client.GetSBOM(context.Background(), imageDigest, syftVersion, false) + require.NoError(t, err) + require.NotNil(t, reader) + defer reader.Close() + assert.True(t, md.Exists) + + // Read in 13-byte sips to also stress the partial-Read consumer path. + var collected []byte + sip := make([]byte, 13) + for { + n, err := reader.Read(sip) + collected = append(collected, sip[:n]...) + if err == io.EOF { + break } - }) - } + require.NoError(t, err) + } + assert.Equal(t, payload, collected, "bytes received over many chunks must match the originally marshaled payload") + + got := &v1beta1.SBOMSyft{} + require.NoError(t, got.Unmarshal(collected)) + assert.Equal(t, original.Name, got.Name) + }) +} + +// TestStorageClient_PutSBOM_NilReader and the next test cover the trivial +// argument-validation paths that the bufconn round-trip doesn't exercise. +func TestStorageClient_PutSBOM_NilReader(t *testing.T) { + _, client, cleanup := startBufconnSBOMServer(t) + defer cleanup() + + _, err := client.PutSBOM(context.Background(), "abc", "1.0.0", proto.SBOMSource_SBOM_SOURCE_WORKLOAD, nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "nil") +} + +func TestMarshalSBOM_NilInput(t *testing.T) { + _, err := MarshalSBOM(nil) + require.Error(t, err) +} + +func TestUnmarshalSBOM_NilReader(t *testing.T) { + _, err := UnmarshalSBOM(nil) + require.Error(t, err) } From c1abadbc6c492adb1a0f6345cff3123fe2bc24ca Mon Sep 17 00:00:00 2001 From: jnathangreeg Date: Tue, 12 May 2026 12:01:12 +0300 Subject: [PATCH 3/6] NAUT-1310: add Stream suffix + CP streaming variants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per discussion on PR #50: - Rename PutSBOM/GetSBOM to PutSBOMStream/GetSBOMStream (proto + Go client + bufconn test server). Suffix makes the streaming nature explicit and leaves the naming room for unary helpers later if needed. - Add SendContainerProfileStream (client-streaming) and GetContainerProfileStream (server-streaming) RPCs as the proper fix for the latent 4 MiB cliff in SendContainerProfile / GetProfile. The entry-count cap on ContainerProfile only loosely correlates with serialized byte size — a profile with high MaxContainerProfileSize or unusually large individual entries can still fail silently on the wire. - Mark `rpc SendContainerProfile` as `option deprecated = true` and add a Deprecated: doc comment on the Go wrapper. GetProfile is left alone because it serves ApplicationProfile and NetworkNeighborhood too — those need their own streaming variants if they have the same risk. (Out of scope for this PR; separate ticket.) - Add a bufconn round-trip test for CP streaming mirroring the existing SBOM round-trip test: send → verify server received intact bytes → fetch with payload split across small chunks → verify reassembly. Renamed sbomRoundTripServer to storageRoundTripServer since it now hosts both SBOM and CP handlers. Co-Authored-By: Claude Opus 4.7 (1M context) --- pkg/client/v1/proto/storage_service.pb.go | 461 ++++++++++++++---- pkg/client/v1/proto/storage_service.proto | 118 ++++- .../v1/proto/storage_service_grpc.pb.go | 219 +++++++-- pkg/client/v1/storageclient.go | 154 +++++- pkg/client/v1/storageclient_test.go | 222 +++++++-- 5 files changed, 988 insertions(+), 186 deletions(-) diff --git a/pkg/client/v1/proto/storage_service.pb.go b/pkg/client/v1/proto/storage_service.pb.go index e3ae830..f8d4158 100644 --- a/pkg/client/v1/proto/storage_service.pb.go +++ b/pkg/client/v1/proto/storage_service.pb.go @@ -673,6 +673,282 @@ func (m *ListNetworkNeighborhoodsResponse) GetCont() string { return "" } +// ContainerProfileChunk is a single chunk of a streamed ContainerProfile +// upload (SendContainerProfileStream). The first chunk in the stream MUST +// set metadata. Subsequent chunks set only blob_chunk. Concatenated +// blob_chunk values form the marshaled ContainerProfile proto payload. +type ContainerProfileChunk struct { + // metadata is set on the first chunk only. + Metadata *ContainerProfileChunkMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // blob_chunk is a slice of the marshaled ContainerProfile payload. + BlobChunk []byte `protobuf:"bytes,2,opt,name=blob_chunk,json=blobChunk,proto3" json:"blob_chunk,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ContainerProfileChunk) Reset() { *m = ContainerProfileChunk{} } +func (m *ContainerProfileChunk) String() string { return proto.CompactTextString(m) } +func (*ContainerProfileChunk) ProtoMessage() {} +func (*ContainerProfileChunk) Descriptor() ([]byte, []int) { + return fileDescriptor_3d90829bc66d9c54, []int{8} +} +func (m *ContainerProfileChunk) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ContainerProfileChunk.Unmarshal(m, b) +} +func (m *ContainerProfileChunk) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ContainerProfileChunk.Marshal(b, m, deterministic) +} +func (m *ContainerProfileChunk) XXX_Merge(src proto.Message) { + xxx_messageInfo_ContainerProfileChunk.Merge(m, src) +} +func (m *ContainerProfileChunk) XXX_Size() int { + return xxx_messageInfo_ContainerProfileChunk.Size(m) +} +func (m *ContainerProfileChunk) XXX_DiscardUnknown() { + xxx_messageInfo_ContainerProfileChunk.DiscardUnknown(m) +} + +var xxx_messageInfo_ContainerProfileChunk proto.InternalMessageInfo + +func (m *ContainerProfileChunk) GetMetadata() *ContainerProfileChunkMetadata { + if m != nil { + return m.Metadata + } + return nil +} + +func (m *ContainerProfileChunk) GetBlobChunk() []byte { + if m != nil { + return m.BlobChunk + } + return nil +} + +// ContainerProfileChunkMetadata is the metadata header sent on the first +// chunk of a SendContainerProfileStream call. All identifying fields +// (cluster, host_type, host_id) continue to be carried via gRPC metadata +// headers as in the unary path; this message is reserved for any future +// per-call options that the unary form would have put in the request. +type ContainerProfileChunkMetadata struct { + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ContainerProfileChunkMetadata) Reset() { *m = ContainerProfileChunkMetadata{} } +func (m *ContainerProfileChunkMetadata) String() string { return proto.CompactTextString(m) } +func (*ContainerProfileChunkMetadata) ProtoMessage() {} +func (*ContainerProfileChunkMetadata) Descriptor() ([]byte, []int) { + return fileDescriptor_3d90829bc66d9c54, []int{9} +} +func (m *ContainerProfileChunkMetadata) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_ContainerProfileChunkMetadata.Unmarshal(m, b) +} +func (m *ContainerProfileChunkMetadata) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_ContainerProfileChunkMetadata.Marshal(b, m, deterministic) +} +func (m *ContainerProfileChunkMetadata) XXX_Merge(src proto.Message) { + xxx_messageInfo_ContainerProfileChunkMetadata.Merge(m, src) +} +func (m *ContainerProfileChunkMetadata) XXX_Size() int { + return xxx_messageInfo_ContainerProfileChunkMetadata.Size(m) +} +func (m *ContainerProfileChunkMetadata) XXX_DiscardUnknown() { + xxx_messageInfo_ContainerProfileChunkMetadata.DiscardUnknown(m) +} + +var xxx_messageInfo_ContainerProfileChunkMetadata proto.InternalMessageInfo + +// GetContainerProfileStreamRequest mirrors the ContainerProfile-shaped +// fields of GetProfileRequest. customer_guid, cluster, host_type, and +// host_id are sent via gRPC metadata headers. +type GetContainerProfileStreamRequest struct { + // Namespace of the workload. + Namespace string `protobuf:"bytes,1,opt,name=namespace,proto3" json:"namespace,omitempty"` + // Name of the container profile to fetch. + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + // Region of the resource (non-k8s scope identifier). + Region string `protobuf:"bytes,3,opt,name=region,proto3" json:"region,omitempty"` + // CloudAccountIdentifier of the resource (non-k8s scope identifier). + CloudAccountIdentifier string `protobuf:"bytes,4,opt,name=cloud_account_identifier,json=cloudAccountIdentifier,proto3" json:"cloud_account_identifier,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetContainerProfileStreamRequest) Reset() { *m = GetContainerProfileStreamRequest{} } +func (m *GetContainerProfileStreamRequest) String() string { return proto.CompactTextString(m) } +func (*GetContainerProfileStreamRequest) ProtoMessage() {} +func (*GetContainerProfileStreamRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_3d90829bc66d9c54, []int{10} +} +func (m *GetContainerProfileStreamRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetContainerProfileStreamRequest.Unmarshal(m, b) +} +func (m *GetContainerProfileStreamRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetContainerProfileStreamRequest.Marshal(b, m, deterministic) +} +func (m *GetContainerProfileStreamRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetContainerProfileStreamRequest.Merge(m, src) +} +func (m *GetContainerProfileStreamRequest) XXX_Size() int { + return xxx_messageInfo_GetContainerProfileStreamRequest.Size(m) +} +func (m *GetContainerProfileStreamRequest) XXX_DiscardUnknown() { + xxx_messageInfo_GetContainerProfileStreamRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_GetContainerProfileStreamRequest proto.InternalMessageInfo + +func (m *GetContainerProfileStreamRequest) GetNamespace() string { + if m != nil { + return m.Namespace + } + return "" +} + +func (m *GetContainerProfileStreamRequest) GetName() string { + if m != nil { + return m.Name + } + return "" +} + +func (m *GetContainerProfileStreamRequest) GetRegion() string { + if m != nil { + return m.Region + } + return "" +} + +func (m *GetContainerProfileStreamRequest) GetCloudAccountIdentifier() string { + if m != nil { + return m.CloudAccountIdentifier + } + return "" +} + +// GetContainerProfileStreamChunk is a single chunk of a streamed +// ContainerProfile download. The first chunk sets metadata + status; +// subsequent chunks set only blob_chunk. +type GetContainerProfileStreamChunk struct { + // metadata is set on the first chunk only. + Metadata *GetContainerProfileStreamChunkMetadata `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + // blob_chunk is a slice of the marshaled ContainerProfile payload. + BlobChunk []byte `protobuf:"bytes,2,opt,name=blob_chunk,json=blobChunk,proto3" json:"blob_chunk,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetContainerProfileStreamChunk) Reset() { *m = GetContainerProfileStreamChunk{} } +func (m *GetContainerProfileStreamChunk) String() string { return proto.CompactTextString(m) } +func (*GetContainerProfileStreamChunk) ProtoMessage() {} +func (*GetContainerProfileStreamChunk) Descriptor() ([]byte, []int) { + return fileDescriptor_3d90829bc66d9c54, []int{11} +} +func (m *GetContainerProfileStreamChunk) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetContainerProfileStreamChunk.Unmarshal(m, b) +} +func (m *GetContainerProfileStreamChunk) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetContainerProfileStreamChunk.Marshal(b, m, deterministic) +} +func (m *GetContainerProfileStreamChunk) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetContainerProfileStreamChunk.Merge(m, src) +} +func (m *GetContainerProfileStreamChunk) XXX_Size() int { + return xxx_messageInfo_GetContainerProfileStreamChunk.Size(m) +} +func (m *GetContainerProfileStreamChunk) XXX_DiscardUnknown() { + xxx_messageInfo_GetContainerProfileStreamChunk.DiscardUnknown(m) +} + +var xxx_messageInfo_GetContainerProfileStreamChunk proto.InternalMessageInfo + +func (m *GetContainerProfileStreamChunk) GetMetadata() *GetContainerProfileStreamChunkMetadata { + if m != nil { + return m.Metadata + } + return nil +} + +func (m *GetContainerProfileStreamChunk) GetBlobChunk() []byte { + if m != nil { + return m.BlobChunk + } + return nil +} + +// GetContainerProfileStreamChunkMetadata is the status header sent on the +// first chunk of a GetContainerProfileStream response. If success is +// false or exists is false the server closes the stream without sending +// blob chunks. +type GetContainerProfileStreamChunkMetadata struct { + Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"` + ErrorMessage string `protobuf:"bytes,2,opt,name=error_message,json=errorMessage,proto3" json:"error_message,omitempty"` + ErrorCode ErrorCode `protobuf:"varint,3,opt,name=error_code,json=errorCode,proto3,enum=storageserver.v1.ErrorCode" json:"error_code,omitempty"` + // exists indicates whether a ContainerProfile row was found. + Exists bool `protobuf:"varint,4,opt,name=exists,proto3" json:"exists,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *GetContainerProfileStreamChunkMetadata) Reset() { + *m = GetContainerProfileStreamChunkMetadata{} +} +func (m *GetContainerProfileStreamChunkMetadata) String() string { return proto.CompactTextString(m) } +func (*GetContainerProfileStreamChunkMetadata) ProtoMessage() {} +func (*GetContainerProfileStreamChunkMetadata) Descriptor() ([]byte, []int) { + return fileDescriptor_3d90829bc66d9c54, []int{12} +} +func (m *GetContainerProfileStreamChunkMetadata) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_GetContainerProfileStreamChunkMetadata.Unmarshal(m, b) +} +func (m *GetContainerProfileStreamChunkMetadata) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_GetContainerProfileStreamChunkMetadata.Marshal(b, m, deterministic) +} +func (m *GetContainerProfileStreamChunkMetadata) XXX_Merge(src proto.Message) { + xxx_messageInfo_GetContainerProfileStreamChunkMetadata.Merge(m, src) +} +func (m *GetContainerProfileStreamChunkMetadata) XXX_Size() int { + return xxx_messageInfo_GetContainerProfileStreamChunkMetadata.Size(m) +} +func (m *GetContainerProfileStreamChunkMetadata) XXX_DiscardUnknown() { + xxx_messageInfo_GetContainerProfileStreamChunkMetadata.DiscardUnknown(m) +} + +var xxx_messageInfo_GetContainerProfileStreamChunkMetadata proto.InternalMessageInfo + +func (m *GetContainerProfileStreamChunkMetadata) GetSuccess() bool { + if m != nil { + return m.Success + } + return false +} + +func (m *GetContainerProfileStreamChunkMetadata) GetErrorMessage() string { + if m != nil { + return m.ErrorMessage + } + return "" +} + +func (m *GetContainerProfileStreamChunkMetadata) GetErrorCode() ErrorCode { + if m != nil { + return m.ErrorCode + } + return ErrorCode_ERROR_CODE_UNSPECIFIED +} + +func (m *GetContainerProfileStreamChunkMetadata) GetExists() bool { + if m != nil { + return m.Exists + } + return false +} + // PutSBOMResponse indicates success or failure of the operation. S3 + // Postgres persistence happens asynchronously via Pulsar; success here // means the upload was accepted and the Pulsar message was published. @@ -689,7 +965,7 @@ func (m *PutSBOMResponse) Reset() { *m = PutSBOMResponse{} } func (m *PutSBOMResponse) String() string { return proto.CompactTextString(m) } func (*PutSBOMResponse) ProtoMessage() {} func (*PutSBOMResponse) Descriptor() ([]byte, []int) { - return fileDescriptor_3d90829bc66d9c54, []int{8} + return fileDescriptor_3d90829bc66d9c54, []int{13} } func (m *PutSBOMResponse) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_PutSBOMResponse.Unmarshal(m, b) @@ -748,7 +1024,7 @@ func (m *PutSBOMChunk) Reset() { *m = PutSBOMChunk{} } func (m *PutSBOMChunk) String() string { return proto.CompactTextString(m) } func (*PutSBOMChunk) ProtoMessage() {} func (*PutSBOMChunk) Descriptor() ([]byte, []int) { - return fileDescriptor_3d90829bc66d9c54, []int{9} + return fileDescriptor_3d90829bc66d9c54, []int{14} } func (m *PutSBOMChunk) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_PutSBOMChunk.Unmarshal(m, b) @@ -797,7 +1073,7 @@ func (m *PutSBOMChunkMetadata) Reset() { *m = PutSBOMChunkMetadata{} } func (m *PutSBOMChunkMetadata) String() string { return proto.CompactTextString(m) } func (*PutSBOMChunkMetadata) ProtoMessage() {} func (*PutSBOMChunkMetadata) Descriptor() ([]byte, []int) { - return fileDescriptor_3d90829bc66d9c54, []int{10} + return fileDescriptor_3d90829bc66d9c54, []int{15} } func (m *PutSBOMChunkMetadata) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_PutSBOMChunkMetadata.Unmarshal(m, b) @@ -861,7 +1137,7 @@ func (m *GetSBOMRequest) Reset() { *m = GetSBOMRequest{} } func (m *GetSBOMRequest) String() string { return proto.CompactTextString(m) } func (*GetSBOMRequest) ProtoMessage() {} func (*GetSBOMRequest) Descriptor() ([]byte, []int) { - return fileDescriptor_3d90829bc66d9c54, []int{11} + return fileDescriptor_3d90829bc66d9c54, []int{16} } func (m *GetSBOMRequest) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_GetSBOMRequest.Unmarshal(m, b) @@ -918,7 +1194,7 @@ func (m *GetSBOMChunk) Reset() { *m = GetSBOMChunk{} } func (m *GetSBOMChunk) String() string { return proto.CompactTextString(m) } func (*GetSBOMChunk) ProtoMessage() {} func (*GetSBOMChunk) Descriptor() ([]byte, []int) { - return fileDescriptor_3d90829bc66d9c54, []int{12} + return fileDescriptor_3d90829bc66d9c54, []int{17} } func (m *GetSBOMChunk) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_GetSBOMChunk.Unmarshal(m, b) @@ -971,7 +1247,7 @@ func (m *GetSBOMChunkMetadata) Reset() { *m = GetSBOMChunkMetadata{} } func (m *GetSBOMChunkMetadata) String() string { return proto.CompactTextString(m) } func (*GetSBOMChunkMetadata) ProtoMessage() {} func (*GetSBOMChunkMetadata) Descriptor() ([]byte, []int) { - return fileDescriptor_3d90829bc66d9c54, []int{13} + return fileDescriptor_3d90829bc66d9c54, []int{18} } func (m *GetSBOMChunkMetadata) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_GetSBOMChunkMetadata.Unmarshal(m, b) @@ -1051,7 +1327,7 @@ func (m *SBOMMetadata) Reset() { *m = SBOMMetadata{} } func (m *SBOMMetadata) String() string { return proto.CompactTextString(m) } func (*SBOMMetadata) ProtoMessage() {} func (*SBOMMetadata) Descriptor() ([]byte, []int) { - return fileDescriptor_3d90829bc66d9c54, []int{14} + return fileDescriptor_3d90829bc66d9c54, []int{19} } func (m *SBOMMetadata) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_SBOMMetadata.Unmarshal(m, b) @@ -1124,6 +1400,11 @@ func init() { proto.RegisterType((*ListApplicationProfilesResponse)(nil), "storageserver.v1.ListApplicationProfilesResponse") proto.RegisterType((*ListNetworkNeighborhoodsRequest)(nil), "storageserver.v1.ListNetworkNeighborhoodsRequest") proto.RegisterType((*ListNetworkNeighborhoodsResponse)(nil), "storageserver.v1.ListNetworkNeighborhoodsResponse") + proto.RegisterType((*ContainerProfileChunk)(nil), "storageserver.v1.ContainerProfileChunk") + proto.RegisterType((*ContainerProfileChunkMetadata)(nil), "storageserver.v1.ContainerProfileChunkMetadata") + proto.RegisterType((*GetContainerProfileStreamRequest)(nil), "storageserver.v1.GetContainerProfileStreamRequest") + proto.RegisterType((*GetContainerProfileStreamChunk)(nil), "storageserver.v1.GetContainerProfileStreamChunk") + proto.RegisterType((*GetContainerProfileStreamChunkMetadata)(nil), "storageserver.v1.GetContainerProfileStreamChunkMetadata") proto.RegisterType((*PutSBOMResponse)(nil), "storageserver.v1.PutSBOMResponse") proto.RegisterType((*PutSBOMChunk)(nil), "storageserver.v1.PutSBOMChunk") proto.RegisterType((*PutSBOMChunkMetadata)(nil), "storageserver.v1.PutSBOMChunkMetadata") @@ -1136,83 +1417,91 @@ func init() { func init() { proto.RegisterFile("storage_service.proto", fileDescriptor_3d90829bc66d9c54) } var fileDescriptor_3d90829bc66d9c54 = []byte{ - // 1244 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x58, 0x4d, 0x6f, 0x1b, 0xc5, - 0x1b, 0xff, 0x6f, 0x5e, 0x9c, 0xf8, 0x89, 0xd3, 0x6e, 0xa7, 0x6e, 0xea, 0xbf, 0x9b, 0x06, 0xd7, - 0x45, 0x55, 0x54, 0xc1, 0xba, 0x09, 0x1c, 0x10, 0x37, 0xc7, 0xde, 0xa6, 0x16, 0x8e, 0xd7, 0x9d, - 0xb5, 0x5b, 0xd1, 0xcb, 0x6a, 0xbd, 0x9e, 0x38, 0x2b, 0xdb, 0x3b, 0xee, 0xce, 0x38, 0x25, 0x45, - 0x42, 0x20, 0x24, 0x04, 0x82, 0x13, 0x37, 0x6e, 0xdc, 0x38, 0xf0, 0x0d, 0xe8, 0x81, 0x0b, 0x9f, - 0x87, 0xaf, 0x00, 0x9a, 0xd9, 0x5d, 0xdb, 0xb5, 0xd7, 0x56, 0x23, 0xa5, 0x04, 0x4e, 0xde, 0xf9, - 0x3d, 0x2f, 0xf3, 0xf3, 0xf3, 0xfc, 0x76, 0x5e, 0x16, 0x6e, 0x30, 0x4e, 0x7d, 0xbb, 0x43, 0x2c, - 0x46, 0xfc, 0x53, 0xd7, 0x21, 0xda, 0xc0, 0xa7, 0x9c, 0x22, 0x35, 0x84, 0x05, 0x4a, 0x7c, 0xed, - 0x74, 0x2f, 0xfb, 0xb8, 0xe3, 0xf2, 0x93, 0x61, 0x4b, 0x73, 0x68, 0xbf, 0xd0, 0x1d, 0xb6, 0x08, - 0x73, 0xec, 0x01, 0x29, 0x84, 0x6e, 0x85, 0x41, 0xb7, 0x53, 0xb0, 0x07, 0x2e, 0x2b, 0x30, 0x7a, - 0xcc, 0x5f, 0xd8, 0x3e, 0x71, 0x68, 0x7f, 0x40, 0x99, 0xcb, 0x5d, 0xea, 0x15, 0x4e, 0xf7, 0x5a, - 0x84, 0xdb, 0x7b, 0x85, 0x0e, 0xf1, 0x88, 0x6f, 0x73, 0xd2, 0x0e, 0x26, 0xc9, 0xff, 0xac, 0xc0, - 0x2d, 0x93, 0x78, 0xed, 0x12, 0xf5, 0xb8, 0xed, 0x7a, 0xc4, 0xaf, 0xfb, 0xf4, 0xd8, 0xed, 0x11, - 0x4c, 0x9e, 0x0f, 0x09, 0xe3, 0xe8, 0x4b, 0x05, 0xae, 0x39, 0x91, 0xcd, 0x1a, 0x04, 0xc6, 0x8c, - 0x92, 0x53, 0x76, 0x37, 0xf6, 0x4d, 0x6d, 0xcc, 0x47, 0x1b, 0xf1, 0xd1, 0x42, 0x3e, 0xda, 0xa0, - 0xdb, 0xd1, 0x04, 0x1f, 0x2d, 0x86, 0x8f, 0x16, 0xf2, 0xd1, 0x66, 0xe6, 0x55, 0x9d, 0x29, 0x24, - 0xff, 0x93, 0x02, 0xdb, 0xf1, 0x14, 0xd9, 0x80, 0x7a, 0x8c, 0xa0, 0x0c, 0xac, 0xb1, 0xa1, 0xe3, - 0x10, 0xc6, 0x24, 0xb1, 0x75, 0x1c, 0x0d, 0xd1, 0x5d, 0xd8, 0x24, 0xbe, 0x4f, 0x7d, 0xab, 0x4f, - 0x18, 0xb3, 0x3b, 0x24, 0xb3, 0x94, 0x53, 0x76, 0x93, 0x38, 0x25, 0xc1, 0xa3, 0x00, 0x43, 0x1f, - 0x03, 0x04, 0x4e, 0x0e, 0x6d, 0x93, 0xcc, 0x72, 0x4e, 0xd9, 0xbd, 0xb2, 0x7f, 0x4b, 0x9b, 0x2e, - 0xbe, 0xa6, 0x0b, 0x9f, 0x12, 0x6d, 0x13, 0x9c, 0x24, 0xd1, 0x63, 0xfe, 0x57, 0x05, 0xae, 0x1d, - 0x12, 0x3e, 0x55, 0x34, 0x04, 0x2b, 0x5d, 0xd7, 0x6b, 0x4b, 0x36, 0x49, 0x2c, 0x9f, 0xd1, 0x36, - 0x24, 0x3d, 0xbb, 0x4f, 0xd8, 0xc0, 0x76, 0x22, 0x1a, 0x63, 0x40, 0x44, 0x88, 0x81, 0x9c, 0x3d, - 0x89, 0xe5, 0x33, 0xda, 0x82, 0x84, 0x4f, 0x3a, 0x2e, 0xf5, 0x32, 0x2b, 0x12, 0x0d, 0x47, 0xe8, - 0x23, 0xc8, 0x38, 0x3d, 0x3a, 0x6c, 0x5b, 0xb6, 0xe3, 0xd0, 0xa1, 0xc7, 0x2d, 0xb7, 0x4d, 0x3c, - 0xee, 0x1e, 0xbb, 0xc4, 0xcf, 0xac, 0x4a, 0xcf, 0x2d, 0x69, 0x2f, 0x06, 0xe6, 0xca, 0xc8, 0x9a, - 0xff, 0x65, 0x05, 0xd0, 0x24, 0xdb, 0x4b, 0xaf, 0x1f, 0xfa, 0x46, 0x81, 0xeb, 0xf6, 0x60, 0xd0, - 0x73, 0x1d, 0x5b, 0xc8, 0x62, 0x24, 0xb0, 0x15, 0x29, 0xb0, 0xe6, 0x05, 0x08, 0xac, 0x38, 0xce, - 0x1e, 0xfd, 0x6f, 0x64, 0xcf, 0x60, 0xe8, 0x3b, 0x05, 0xd2, 0x1e, 0xe1, 0x2f, 0xa8, 0xdf, 0xb5, - 0x3c, 0xe2, 0x76, 0x4e, 0x5a, 0xd4, 0x3f, 0xa1, 0xb4, 0x2d, 0x2b, 0xba, 0xb1, 0xff, 0xe4, 0x02, - 0x98, 0xd4, 0x82, 0xf4, 0xb5, 0x89, 0xec, 0xf8, 0xba, 0x37, 0x0b, 0xce, 0x79, 0xe7, 0x12, 0xff, - 0xe4, 0x3b, 0xf7, 0x9b, 0x02, 0x3b, 0x55, 0x97, 0xf1, 0xd9, 0xea, 0xb1, 0x48, 0xe4, 0xaf, 0x09, - 0x5a, 0x99, 0x16, 0x74, 0x1a, 0x56, 0x7b, 0x6e, 0xdf, 0xe5, 0xb2, 0x93, 0xcb, 0x38, 0x18, 0x08, - 0x99, 0x8b, 0xa9, 0x42, 0x99, 0xca, 0xe7, 0x09, 0x99, 0x27, 0xde, 0x58, 0xe6, 0x6b, 0x0b, 0x65, - 0xfe, 0x6a, 0x09, 0xde, 0x99, 0x4b, 0xfe, 0xf2, 0x35, 0xff, 0xad, 0x02, 0xe9, 0x18, 0xcd, 0xb3, - 0xcc, 0x4a, 0x6e, 0xf9, 0xed, 0x89, 0xfe, 0xfa, 0xac, 0xe8, 0x59, 0x5c, 0x3f, 0xf2, 0xaf, 0x94, - 0xa0, 0x7a, 0x31, 0x72, 0xfd, 0x0f, 0xf4, 0xfe, 0xf7, 0x25, 0xc8, 0xcd, 0x67, 0x7f, 0xf9, 0xcd, - 0xff, 0x5e, 0x81, 0x1b, 0x71, 0xeb, 0x4c, 0xd4, 0xfd, 0xb7, 0xb5, 0xd0, 0xa4, 0x63, 0x16, 0x9a, - 0xf8, 0xfe, 0xff, 0xa0, 0xc0, 0xd5, 0xfa, 0x90, 0x9b, 0x07, 0xc6, 0xd1, 0xbf, 0x61, 0x87, 0x7d, - 0x0e, 0xa9, 0x90, 0x4d, 0xe9, 0x64, 0xe8, 0x75, 0xd1, 0x01, 0xac, 0xf7, 0x09, 0xb7, 0xdb, 0x36, - 0xb7, 0xc3, 0x63, 0xc8, 0xbd, 0xd9, 0x4c, 0x93, 0x11, 0x47, 0xa1, 0x37, 0x1e, 0xc5, 0xa1, 0xdb, - 0x00, 0xad, 0x1e, 0x6d, 0x59, 0x8e, 0xb0, 0x4b, 0xc6, 0x29, 0x9c, 0x14, 0x88, 0x0c, 0xc8, 0xff, - 0xa8, 0x40, 0x3a, 0x2e, 0x03, 0xba, 0x03, 0x29, 0xb7, 0x2f, 0x0e, 0x6a, 0x6d, 0xb7, 0x43, 0x18, - 0x0f, 0x95, 0xbf, 0x21, 0xb1, 0xb2, 0x84, 0x84, 0x0b, 0x3b, 0x3b, 0xe6, 0xd6, 0x29, 0xf1, 0x99, - 0xd0, 0x75, 0x50, 0x8e, 0x0d, 0x81, 0x3d, 0x09, 0x20, 0xf4, 0x21, 0x24, 0x18, 0x1d, 0xfa, 0x4e, - 0x54, 0x89, 0xed, 0x59, 0xfe, 0x62, 0x6a, 0x53, 0xfa, 0xe0, 0xd0, 0x37, 0xff, 0x39, 0x5c, 0x39, - 0x24, 0x61, 0x57, 0x82, 0x97, 0xf0, 0x62, 0xd8, 0xdc, 0x85, 0xcd, 0xa8, 0x2e, 0x16, 0xf5, 0x7a, - 0x67, 0x92, 0xd4, 0x3a, 0x4e, 0x45, 0xa0, 0xe1, 0xf5, 0xce, 0x44, 0x13, 0xc2, 0xc9, 0xcf, 0xd1, - 0x84, 0xc9, 0x88, 0xf3, 0x37, 0xe1, 0x4f, 0x05, 0xd2, 0x71, 0x19, 0x2e, 0xf3, 0xe5, 0xdd, 0x82, - 0x04, 0xf9, 0xcc, 0x65, 0x9c, 0xc9, 0x95, 0x6d, 0x1d, 0x87, 0x23, 0x54, 0x82, 0x4d, 0xd6, 0xa2, - 0x7d, 0x6b, 0x54, 0x93, 0xe0, 0xd0, 0xb0, 0x13, 0xdf, 0xd8, 0x51, 0x2d, 0x52, 0x22, 0x28, 0x1a, - 0xe5, 0xff, 0x52, 0x20, 0x35, 0x69, 0xbe, 0x4c, 0xb5, 0xa1, 0x7b, 0x70, 0x55, 0x36, 0x87, 0xb9, - 0x2f, 0x89, 0xd5, 0x3a, 0xe3, 0x84, 0x85, 0x8b, 0xf9, 0xa6, 0x80, 0x4d, 0xf7, 0x25, 0x39, 0x10, - 0xa0, 0x68, 0xa2, 0xe3, 0x13, 0x71, 0x9f, 0xb0, 0xec, 0x68, 0x19, 0x49, 0x86, 0x48, 0x91, 0xa3, - 0xf7, 0x00, 0xf5, 0x6c, 0xc6, 0x2d, 0x9f, 0x1c, 0x13, 0x9f, 0x78, 0x4e, 0xe0, 0x16, 0xac, 0xf5, - 0xaa, 0xb0, 0xe0, 0x91, 0xa1, 0xc8, 0xef, 0xff, 0xb1, 0x04, 0xc9, 0x51, 0xdd, 0x51, 0x16, 0xb6, - 0x74, 0x8c, 0x0d, 0x6c, 0x95, 0x8c, 0xb2, 0x6e, 0x35, 0x6b, 0x66, 0x5d, 0x2f, 0x55, 0x1e, 0x56, - 0xf4, 0xb2, 0xfa, 0x3f, 0xb4, 0x03, 0xd9, 0x09, 0x5b, 0xa5, 0xf6, 0xa4, 0x58, 0xad, 0x94, 0x2d, - 0xac, 0x3f, 0x6e, 0xea, 0x66, 0x43, 0x55, 0xd0, 0x2d, 0xb8, 0xf9, 0x5a, 0x6c, 0xb1, 0xd9, 0x78, - 0x64, 0xe0, 0xca, 0x33, 0xbd, 0xac, 0x2e, 0xa1, 0x1c, 0x6c, 0x4f, 0x18, 0xeb, 0xd8, 0x78, 0x58, - 0xa9, 0xea, 0x56, 0xc3, 0x30, 0xac, 0x6a, 0x11, 0x1f, 0xea, 0xea, 0xf2, 0x1c, 0x8f, 0x92, 0x71, - 0x54, 0xaf, 0xea, 0x0d, 0xbd, 0xac, 0xae, 0xcc, 0xf1, 0xa8, 0x19, 0x0d, 0xeb, 0xa1, 0xd1, 0xac, - 0x95, 0xd5, 0x55, 0x74, 0x1b, 0xfe, 0xff, 0x1a, 0xc5, 0x86, 0x8e, 0x6b, 0xc5, 0xaa, 0x25, 0x31, - 0x35, 0x31, 0xc5, 0xb0, 0xde, 0xac, 0x9a, 0x45, 0x1c, 0x1a, 0xd7, 0xa6, 0x62, 0x45, 0x7b, 0x26, - 0x52, 0xaf, 0xc7, 0x99, 0xc7, 0xec, 0x93, 0xf7, 0x19, 0xc0, 0xb8, 0xa3, 0x62, 0x22, 0xe9, 0x61, - 0x1a, 0x4d, 0x5c, 0x9a, 0xae, 0x63, 0x06, 0xd2, 0x93, 0xc6, 0xa7, 0x06, 0xfe, 0xa4, 0x6a, 0x14, - 0xcb, 0xaa, 0x32, 0x6d, 0xc1, 0xfa, 0x61, 0xc5, 0x6c, 0xe0, 0x4f, 0xd5, 0x25, 0x94, 0x06, 0x75, - 0xd2, 0xf2, 0xc8, 0x30, 0x1b, 0xea, 0xf2, 0xfe, 0xd7, 0xab, 0x70, 0xc5, 0x0c, 0x84, 0x65, 0x06, - 0xb7, 0x58, 0x34, 0x84, 0x74, 0xdc, 0xb5, 0x0d, 0xbd, 0x1f, 0xa3, 0xc0, 0xf9, 0x37, 0xd0, 0xac, - 0xf6, 0xa6, 0xee, 0xe1, 0x5e, 0xf5, 0x14, 0x60, 0x7c, 0xc7, 0x41, 0x77, 0x63, 0xd7, 0xa5, 0xa9, - 0x29, 0xde, 0x5d, 0xec, 0x14, 0x26, 0xfe, 0x02, 0x6e, 0xce, 0x39, 0x55, 0xa2, 0x07, 0xb3, 0x09, - 0x16, 0x9f, 0x9e, 0xb3, 0x7b, 0xe7, 0x88, 0x08, 0xe7, 0xff, 0x4a, 0x81, 0xcc, 0xbc, 0xa3, 0x0d, - 0x9a, 0x93, 0x6f, 0xc1, 0x21, 0x2e, 0xbb, 0x7f, 0x9e, 0x90, 0x90, 0x43, 0x0d, 0xd6, 0xc2, 0x9d, - 0x11, 0xed, 0x2c, 0xde, 0x76, 0xb3, 0x77, 0xe6, 0xda, 0xa3, 0x6c, 0xbb, 0x0a, 0x3a, 0x82, 0xb5, - 0x70, 0x91, 0x47, 0xb9, 0xb9, 0x3b, 0x48, 0x44, 0x78, 0x67, 0xf1, 0x1e, 0xf3, 0x40, 0x39, 0xd8, - 0x7f, 0xf6, 0x20, 0xf6, 0x13, 0x49, 0xcb, 0x76, 0xba, 0xc4, 0x6b, 0xcb, 0x4f, 0x24, 0x4e, 0xcf, - 0x25, 0x1e, 0x2f, 0x9c, 0xee, 0x15, 0xe4, 0x17, 0x90, 0x56, 0x42, 0xfe, 0x7c, 0xf0, 0x77, 0x00, - 0x00, 0x00, 0xff, 0xff, 0x93, 0x62, 0x6e, 0x2f, 0x86, 0x11, 0x00, 0x00, + // 1372 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xd4, 0x58, 0x4f, 0x6f, 0x1a, 0x47, + 0x14, 0xef, 0x00, 0xc1, 0xe6, 0x19, 0x27, 0x9b, 0x09, 0x71, 0x08, 0x71, 0x1c, 0x42, 0xaa, 0xd4, + 0x8a, 0xda, 0xc5, 0xa6, 0x3d, 0x44, 0xbd, 0x61, 0xd8, 0x38, 0x28, 0x98, 0x75, 0x66, 0x21, 0x51, + 0x73, 0x59, 0x2d, 0xcb, 0x18, 0xaf, 0x80, 0x5d, 0xb2, 0x33, 0x38, 0x75, 0x2a, 0x55, 0xad, 0x22, + 0x55, 0xa9, 0xda, 0x53, 0xab, 0x1e, 0x7a, 0xeb, 0xa9, 0x3d, 0xf4, 0x1b, 0x34, 0x87, 0x5e, 0xfa, + 0x79, 0xfa, 0x15, 0x5a, 0xed, 0x3f, 0x4c, 0x60, 0x17, 0x19, 0x29, 0xa9, 0xd3, 0x13, 0xec, 0xfb, + 0xfb, 0xdb, 0xf7, 0x7e, 0x3b, 0xf3, 0x66, 0xe0, 0x32, 0xe3, 0x96, 0xad, 0x75, 0xa9, 0xca, 0xa8, + 0x7d, 0x64, 0xe8, 0x54, 0x1c, 0xda, 0x16, 0xb7, 0xb0, 0xe0, 0x8b, 0x1d, 0x29, 0xb5, 0xc5, 0xa3, + 0xed, 0xdc, 0xc3, 0xae, 0xc1, 0x0f, 0x47, 0x6d, 0x51, 0xb7, 0x06, 0xc5, 0xde, 0xa8, 0x4d, 0x99, + 0xae, 0x0d, 0x69, 0xd1, 0x37, 0x2b, 0x0e, 0x7b, 0xdd, 0xa2, 0x36, 0x34, 0x58, 0x91, 0x59, 0x07, + 0xfc, 0x99, 0x66, 0x53, 0xdd, 0x1a, 0x0c, 0x2d, 0x66, 0x70, 0xc3, 0x32, 0x8b, 0x47, 0xdb, 0x6d, + 0xca, 0xb5, 0xed, 0x62, 0x97, 0x9a, 0xd4, 0xd6, 0x38, 0xed, 0x78, 0x49, 0x0a, 0xbf, 0x20, 0xb8, + 0xa6, 0x50, 0xb3, 0x53, 0xb1, 0x4c, 0xae, 0x19, 0x26, 0xb5, 0xf7, 0x6d, 0xeb, 0xc0, 0xe8, 0x53, + 0x42, 0x9f, 0x8e, 0x28, 0xe3, 0xf8, 0x2b, 0x04, 0x17, 0xf5, 0x40, 0xa7, 0x0e, 0x3d, 0x65, 0x16, + 0xe5, 0xd1, 0xe6, 0x4a, 0x49, 0x11, 0x4f, 0xf0, 0x88, 0x63, 0x3c, 0xa2, 0x8f, 0x47, 0x1c, 0xf6, + 0xba, 0xa2, 0x83, 0x47, 0x0c, 0xc1, 0x23, 0xfa, 0x78, 0xc4, 0x99, 0xbc, 0x82, 0x3e, 0x25, 0x29, + 0xfc, 0x8c, 0x60, 0x3d, 0x1c, 0x22, 0x1b, 0x5a, 0x26, 0xa3, 0x38, 0x0b, 0x4b, 0x6c, 0xa4, 0xeb, + 0x94, 0x31, 0x17, 0xd8, 0x32, 0x09, 0x1e, 0xf1, 0x2d, 0x58, 0xa5, 0xb6, 0x6d, 0xd9, 0xea, 0x80, + 0x32, 0xa6, 0x75, 0x69, 0x36, 0x96, 0x47, 0x9b, 0x29, 0x92, 0x76, 0x85, 0x7b, 0x9e, 0x0c, 0x7f, + 0x0a, 0xe0, 0x19, 0xe9, 0x56, 0x87, 0x66, 0xe3, 0x79, 0xb4, 0x79, 0xbe, 0x74, 0x4d, 0x9c, 0x2e, + 0xbe, 0x28, 0x39, 0x36, 0x15, 0xab, 0x43, 0x49, 0x8a, 0x06, 0x7f, 0x0b, 0xbf, 0x23, 0xb8, 0xb8, + 0x4b, 0xf9, 0x54, 0xd1, 0x30, 0x24, 0x7a, 0x86, 0xd9, 0x71, 0xd1, 0xa4, 0x88, 0xfb, 0x1f, 0xaf, + 0x43, 0xca, 0xd4, 0x06, 0x94, 0x0d, 0x35, 0x3d, 0x80, 0x71, 0x22, 0x70, 0x3c, 0x9c, 0x07, 0x37, + 0x7b, 0x8a, 0xb8, 0xff, 0xf1, 0x1a, 0x24, 0x6d, 0xda, 0x35, 0x2c, 0x33, 0x9b, 0x70, 0xa5, 0xfe, + 0x13, 0xbe, 0x0b, 0x59, 0xbd, 0x6f, 0x8d, 0x3a, 0xaa, 0xa6, 0xeb, 0xd6, 0xc8, 0xe4, 0xaa, 0xd1, + 0xa1, 0x26, 0x37, 0x0e, 0x0c, 0x6a, 0x67, 0xcf, 0xb9, 0x96, 0x6b, 0xae, 0xbe, 0xec, 0xa9, 0x6b, + 0x63, 0x6d, 0xe1, 0xb7, 0x04, 0xe0, 0x49, 0xb4, 0x67, 0x5e, 0x3f, 0xfc, 0x0d, 0x82, 0x4b, 0xda, + 0x70, 0xd8, 0x37, 0x74, 0xcd, 0xa1, 0xc5, 0x98, 0x60, 0x09, 0x97, 0x60, 0xad, 0x37, 0x40, 0xb0, + 0xf2, 0x49, 0xf4, 0xe0, 0xbd, 0xb1, 0x36, 0x23, 0xc3, 0xdf, 0x22, 0xc8, 0x98, 0x94, 0x3f, 0xb3, + 0xec, 0x9e, 0x6a, 0x52, 0xa3, 0x7b, 0xd8, 0xb6, 0xec, 0x43, 0xcb, 0xea, 0xb8, 0x15, 0x5d, 0x29, + 0x3d, 0x7a, 0x03, 0x48, 0x1a, 0x5e, 0xf8, 0xc6, 0x44, 0x74, 0x72, 0xc9, 0x9c, 0x15, 0x46, 0x7c, + 0x73, 0xc9, 0xff, 0xf2, 0x9b, 0xfb, 0x03, 0xc1, 0x46, 0xdd, 0x60, 0x7c, 0xb6, 0x7a, 0x2c, 0x20, + 0xf9, 0x6b, 0x84, 0x46, 0xd3, 0x84, 0xce, 0xc0, 0xb9, 0xbe, 0x31, 0x30, 0xb8, 0xdb, 0xc9, 0x38, + 0xf1, 0x1e, 0x1c, 0x9a, 0x3b, 0xa9, 0x7c, 0x9a, 0xba, 0xff, 0x27, 0x68, 0x9e, 0x3c, 0x35, 0xcd, + 0x97, 0xe6, 0xd2, 0xfc, 0x55, 0x0c, 0x6e, 0x44, 0x82, 0x3f, 0x7b, 0xce, 0xbf, 0x44, 0x90, 0x09, + 0xe1, 0x3c, 0xcb, 0x26, 0xf2, 0xf1, 0xb7, 0x47, 0xfa, 0x4b, 0xb3, 0xa4, 0x67, 0x61, 0xfd, 0x28, + 0xbc, 0x42, 0x5e, 0xf5, 0x42, 0xe8, 0xfa, 0x3f, 0xe8, 0xfd, 0x9f, 0x31, 0xc8, 0x47, 0xa3, 0x3f, + 0xfb, 0xe6, 0x7f, 0x87, 0xe0, 0x72, 0xd8, 0x3a, 0x13, 0x74, 0xff, 0x6d, 0x2d, 0x34, 0x99, 0x90, + 0x85, 0x26, 0xbc, 0xff, 0x2f, 0x10, 0x5c, 0x9e, 0x5e, 0x21, 0x2a, 0x87, 0x23, 0xb3, 0x87, 0x1f, + 0xc0, 0xf2, 0x80, 0x72, 0xad, 0xa3, 0x71, 0xcd, 0x9f, 0x00, 0x8a, 0xb3, 0x6f, 0x1d, 0xea, 0xba, + 0xe7, 0xbb, 0x91, 0x71, 0x00, 0x7c, 0x1d, 0xa0, 0xdd, 0xb7, 0xda, 0xaa, 0xee, 0xe8, 0xdd, 0x32, + 0xa7, 0x49, 0xca, 0x91, 0xb8, 0x0e, 0x85, 0x1b, 0x70, 0x7d, 0x6e, 0xa4, 0xc2, 0xaf, 0x08, 0xf2, + 0xbb, 0x94, 0x4f, 0x1b, 0x29, 0xdc, 0xa6, 0xda, 0xe0, 0x74, 0x3c, 0x0d, 0x36, 0xdd, 0x58, 0xe8, + 0xa6, 0x1b, 0x3f, 0x35, 0x23, 0x13, 0x73, 0x19, 0xf9, 0x13, 0x82, 0x8d, 0x48, 0xa0, 0x5e, 0x61, + 0x9b, 0x33, 0x85, 0xbd, 0x3b, 0x5b, 0xd8, 0xf9, 0x31, 0x16, 0xaf, 0xf0, 0x2b, 0x04, 0xb7, 0x4f, + 0x17, 0xf3, 0x2c, 0xbf, 0x97, 0x35, 0x48, 0xd2, 0xcf, 0x0d, 0xc6, 0x99, 0x5b, 0xe5, 0x65, 0xe2, + 0x3f, 0x15, 0xbe, 0x47, 0x70, 0x61, 0x7f, 0xc4, 0x95, 0x1d, 0x79, 0xef, 0x5d, 0x98, 0x03, 0x9f, + 0x42, 0xda, 0x47, 0xe3, 0x75, 0x74, 0x67, 0xa6, 0xa3, 0xb7, 0x67, 0x23, 0x4d, 0x7a, 0x2c, 0xde, + 0xbf, 0x1f, 0x10, 0x64, 0xc2, 0x22, 0xe0, 0x9b, 0x90, 0x36, 0x06, 0xce, 0x71, 0xa2, 0x63, 0x74, + 0x29, 0xe3, 0x3e, 0xef, 0x57, 0x5c, 0x59, 0xd5, 0x15, 0x39, 0x26, 0xec, 0xf8, 0x80, 0xab, 0x47, + 0xd4, 0x66, 0x0e, 0xd7, 0xbd, 0x72, 0xac, 0x38, 0xb2, 0x47, 0x9e, 0x08, 0x7f, 0x02, 0x49, 0x66, + 0x8d, 0x6c, 0x3d, 0xa8, 0xc4, 0xfa, 0x2c, 0x7e, 0x27, 0xb5, 0xe2, 0xda, 0x10, 0xdf, 0xb6, 0xf0, + 0x05, 0x9c, 0xdf, 0xa5, 0x7e, 0x57, 0xbc, 0x4f, 0xf0, 0xcd, 0xa0, 0xb9, 0x05, 0xab, 0x41, 0x5d, + 0x54, 0xcb, 0xec, 0x1f, 0xbb, 0xa0, 0x96, 0x49, 0x3a, 0x10, 0xca, 0x66, 0xff, 0xd8, 0x69, 0x82, + 0x9f, 0x7c, 0x81, 0x26, 0x4c, 0x7a, 0x2c, 0xde, 0x84, 0xbf, 0x11, 0x64, 0xc2, 0x22, 0xbc, 0x83, + 0x9f, 0x0c, 0xae, 0xc0, 0x2a, 0x6b, 0x5b, 0x03, 0x75, 0x5c, 0x13, 0x6f, 0xb4, 0xdd, 0x08, 0x6f, + 0xec, 0xb8, 0x16, 0x69, 0xc7, 0x69, 0xbc, 0xec, 0xfe, 0x83, 0x20, 0x3d, 0xa9, 0x3e, 0x4b, 0xb6, + 0xe1, 0xdb, 0x70, 0xc1, 0x6d, 0x0e, 0x33, 0x9e, 0x53, 0xb5, 0x7d, 0xcc, 0x29, 0xf3, 0x47, 0x8e, + 0x55, 0x47, 0xac, 0x18, 0xcf, 0xe9, 0x8e, 0x23, 0x74, 0x9a, 0xa8, 0xdb, 0xd4, 0x39, 0xf5, 0xaa, + 0x5a, 0xb0, 0xd9, 0xa5, 0x7c, 0x49, 0x99, 0xe3, 0x0f, 0x01, 0xf7, 0x35, 0xc6, 0x55, 0x9b, 0x1e, + 0x50, 0x9b, 0x9a, 0xba, 0x67, 0xe6, 0x4d, 0x24, 0x82, 0xa3, 0x21, 0x63, 0x45, 0x99, 0xdf, 0xf9, + 0x2b, 0x06, 0xa9, 0x71, 0xdd, 0x71, 0x0e, 0xd6, 0x24, 0x42, 0x64, 0xa2, 0x56, 0xe4, 0xaa, 0xa4, + 0xb6, 0x1a, 0xca, 0xbe, 0x54, 0xa9, 0xdd, 0xab, 0x49, 0x55, 0xe1, 0x3d, 0xbc, 0x01, 0xb9, 0x09, + 0x5d, 0xad, 0xf1, 0xa8, 0x5c, 0xaf, 0x55, 0x55, 0x22, 0x3d, 0x6c, 0x49, 0x4a, 0x53, 0x40, 0xf8, + 0x1a, 0x5c, 0x79, 0xcd, 0xb7, 0xdc, 0x6a, 0xde, 0x97, 0x49, 0xed, 0x89, 0x54, 0x15, 0x62, 0x38, + 0x0f, 0xeb, 0x13, 0xca, 0x7d, 0x22, 0xdf, 0xab, 0xd5, 0x25, 0xb5, 0x29, 0xcb, 0x6a, 0xbd, 0x4c, + 0x76, 0x25, 0x21, 0x1e, 0x61, 0x51, 0x91, 0xf7, 0xf6, 0xeb, 0x52, 0x53, 0xaa, 0x0a, 0x89, 0x08, + 0x8b, 0x86, 0xdc, 0x54, 0xef, 0xc9, 0xad, 0x46, 0x55, 0x38, 0x87, 0xaf, 0xc3, 0xd5, 0xd7, 0x20, + 0x36, 0x25, 0xd2, 0x28, 0xd7, 0x55, 0x57, 0x26, 0x24, 0xa7, 0x10, 0xee, 0xb7, 0xea, 0x4a, 0x99, + 0xf8, 0xca, 0xa5, 0x29, 0x5f, 0xa7, 0x3d, 0x13, 0xa1, 0x97, 0xc3, 0xd4, 0x27, 0xe8, 0x53, 0x77, + 0x18, 0xc0, 0x49, 0x47, 0x9d, 0x44, 0xae, 0x85, 0x22, 0xb7, 0x48, 0x65, 0xba, 0x8e, 0x59, 0xc8, + 0x4c, 0x2a, 0x1f, 0xcb, 0xe4, 0x41, 0x5d, 0x2e, 0x57, 0x05, 0x34, 0xad, 0x21, 0xd2, 0x6e, 0x4d, + 0x69, 0x92, 0xcf, 0x84, 0x18, 0xce, 0x80, 0x30, 0xa9, 0xb9, 0x2f, 0x2b, 0x4d, 0x21, 0x5e, 0xfa, + 0x71, 0x09, 0xce, 0x2b, 0x1e, 0xb1, 0x14, 0xef, 0xae, 0x05, 0x3f, 0x87, 0x4c, 0xd8, 0xe5, 0x02, + 0xfe, 0x28, 0x84, 0x81, 0xd1, 0xf7, 0x24, 0x39, 0xf1, 0xb4, 0xe6, 0xde, 0x5e, 0x55, 0x88, 0xbf, + 0x8c, 0x21, 0xfc, 0x18, 0xe0, 0xe4, 0x38, 0x8e, 0x6f, 0x85, 0x2e, 0x4e, 0x53, 0x79, 0xde, 0x9f, + 0x6f, 0xe4, 0xef, 0x84, 0x5f, 0xc2, 0x95, 0x88, 0x03, 0x10, 0xde, 0x9a, 0x0d, 0x30, 0xff, 0xa0, + 0x97, 0xdb, 0x5e, 0xc0, 0xc3, 0xcf, 0xff, 0x35, 0x82, 0x6c, 0xd4, 0x14, 0x8e, 0x23, 0xe2, 0xcd, + 0x39, 0x6f, 0xe4, 0x4a, 0x8b, 0xb8, 0xf8, 0x18, 0x9e, 0x41, 0x2e, 0xac, 0x03, 0xde, 0x7c, 0x83, + 0x3f, 0x38, 0xe5, 0xe4, 0xba, 0x68, 0x63, 0x37, 0x11, 0x7e, 0x81, 0xe0, 0x6a, 0xe4, 0x60, 0x85, + 0x4b, 0x0b, 0x4c, 0x76, 0xc1, 0xeb, 0x6f, 0x2d, 0x3a, 0x0d, 0x6e, 0x21, 0xdc, 0x84, 0x55, 0x7f, + 0x3a, 0xf0, 0x13, 0x6f, 0xcc, 0x1f, 0x40, 0x72, 0x37, 0x23, 0xf5, 0x13, 0xef, 0xa6, 0xc0, 0xaa, + 0xbf, 0xdd, 0xf9, 0x51, 0xf3, 0x91, 0x3b, 0x6a, 0x00, 0x7e, 0x63, 0xfe, 0x9e, 0xbb, 0x85, 0x76, + 0x4a, 0x4f, 0xb6, 0x42, 0x2f, 0x36, 0xdb, 0x9a, 0xde, 0xa3, 0x66, 0xc7, 0xbd, 0xd8, 0xd4, 0xfb, + 0x06, 0x35, 0x79, 0xf1, 0x68, 0xbb, 0xe8, 0xde, 0x5b, 0xb6, 0x93, 0xee, 0xcf, 0xc7, 0xff, 0x06, + 0x00, 0x00, 0xff, 0xff, 0x28, 0x46, 0xac, 0x2a, 0x3c, 0x15, 0x00, 0x00, } diff --git a/pkg/client/v1/proto/storage_service.proto b/pkg/client/v1/proto/storage_service.proto index fefe18e..df78491 100644 --- a/pkg/client/v1/proto/storage_service.proto +++ b/pkg/client/v1/proto/storage_service.proto @@ -9,11 +9,22 @@ import "github.com/kubescape/storage/pkg/apis/softwarecomposition/v1beta1/genera // StorageService provides gRPC endpoints for node agent to communicate with backend storage service StorageService { // SendContainerProfile receives a container profile (time-series snapshot) from node agent - // and sends it to Pulsar for processing by the ingester - rpc SendContainerProfile(SendContainerProfileRequest) returns (SendContainerProfileResponse); + // and sends it to Pulsar for processing by the ingester. + // + // Deprecated: use SendContainerProfileStream. The unary form is silently + // capped by gRPC's default 4 MiB message limit; container profiles with + // many or large entries (or with MaxContainerProfileSize raised) can + // exceed this and fail on the wire. The streaming variant has no such + // bound. + rpc SendContainerProfile(SendContainerProfileRequest) returns (SendContainerProfileResponse) { + option deprecated = true; + } // GetProfile retrieves an aggregated profile (ApplicationProfile, NetworkNeighborhood, - // or ContainerProfile) by fetching them from S3 + // or ContainerProfile) by fetching them from S3. + // + // Note: for ContainerProfile specifically, prefer GetContainerProfileStream, + // which is not subject to the default 4 MiB unary gRPC message limit. rpc GetProfile(GetProfileRequest) returns (GetProfileResponse); // ListApplicationProfiles lists all ApplicationProfiles in a namespace (returns metadata only, nil Spec) @@ -22,9 +33,30 @@ service StorageService { // ListNetworkNeighborhoods lists all NetworkNeighborhoods in a namespace (returns metadata only, nil Spec) rpc ListNetworkNeighborhoods(ListNetworkNeighborhoodsRequest) returns (ListNetworkNeighborhoodsResponse); - // PutSBOM uploads an SBOM produced for an image. Client-streaming so the - // caller need not know the payload size in advance and can stream bytes - // from any io.Reader. Idempotent on (customer_guid, image_digest, + // SendContainerProfileStream is the streaming replacement for + // SendContainerProfile. Client-streaming so the caller need not know the + // payload size in advance — required because container profiles with + // many or large entries can exceed the default 4 MiB unary gRPC limit. + // + // The first chunk MUST set metadata (which carries identifying labels + // and may be empty otherwise) and MAY include blob_chunk for small + // payloads. Subsequent chunks set only blob_chunk. Concatenated + // blob_chunk values form the marshaled ContainerProfile proto payload. + rpc SendContainerProfileStream(stream ContainerProfileChunk) returns (SendContainerProfileResponse); + + // GetContainerProfileStream is the streaming replacement for + // GetProfile(kind=ContainerProfile). Server-streaming so the caller need + // not know the response size in advance. + // + // The first chunk in the response carries metadata + status. When the + // profile does not exist or success is false, the server closes the + // stream after the metadata chunk. Otherwise subsequent chunks carry + // the marshaled ContainerProfile bytes. + rpc GetContainerProfileStream(GetContainerProfileStreamRequest) returns (stream GetContainerProfileStreamChunk); + + // PutSBOMStream uploads an SBOM produced for an image. Client-streaming + // so the caller need not know the payload size in advance and can stream + // bytes from any io.Reader. Idempotent on (customer_guid, image_digest, // syft_version) — duplicate calls upsert the metadata row and overwrite // the S3 blob; end-state is consistent. // @@ -32,17 +64,17 @@ service StorageService { // and MAY include blob_chunk for small payloads. Subsequent chunks set // only blob_chunk. Concatenated blob_chunk values form the marshaled // SBOMSyft proto payload. - rpc PutSBOM(stream PutSBOMChunk) returns (PutSBOMResponse); + rpc PutSBOMStream(stream PutSBOMChunk) returns (PutSBOMResponse); - // GetSBOM probes for or fetches an SBOM by (customer_guid, image_digest, - // syft_version). Server-streaming so the caller need not know the - // response size in advance. + // GetSBOMStream probes for or fetches an SBOM by (customer_guid, + // image_digest, syft_version). Server-streaming so the caller need not + // know the response size in advance. // // The first chunk in the response carries metadata + status. When the // request sets metadata_only=true OR the row does not exist OR success // is false, the server closes the stream after the metadata chunk. // Otherwise subsequent chunks carry the marshaled SBOMSyft bytes. - rpc GetSBOM(GetSBOMRequest) returns (stream GetSBOMChunk); + rpc GetSBOMStream(GetSBOMRequest) returns (stream GetSBOMChunk); } // SendContainerProfileRequest contains the container profile to be stored @@ -203,6 +235,70 @@ message ListNetworkNeighborhoodsResponse { string cont = 5; } +// ContainerProfileChunk is a single chunk of a streamed ContainerProfile +// upload (SendContainerProfileStream). The first chunk in the stream MUST +// set metadata. Subsequent chunks set only blob_chunk. Concatenated +// blob_chunk values form the marshaled ContainerProfile proto payload. +message ContainerProfileChunk { + // metadata is set on the first chunk only. + ContainerProfileChunkMetadata metadata = 1; + + // blob_chunk is a slice of the marshaled ContainerProfile payload. + bytes blob_chunk = 2; +} + +// ContainerProfileChunkMetadata is the metadata header sent on the first +// chunk of a SendContainerProfileStream call. All identifying fields +// (cluster, host_type, host_id) continue to be carried via gRPC metadata +// headers as in the unary path; this message is reserved for any future +// per-call options that the unary form would have put in the request. +message ContainerProfileChunkMetadata { + // reserved for future use; the message exists so the chunked-protocol + // shape matches PutSBOMChunk and clients can begin streaming today + // without an additional API revision when options are added. +} + +// GetContainerProfileStreamRequest mirrors the ContainerProfile-shaped +// fields of GetProfileRequest. customer_guid, cluster, host_type, and +// host_id are sent via gRPC metadata headers. +message GetContainerProfileStreamRequest { + // Namespace of the workload. + string namespace = 1; + + // Name of the container profile to fetch. + string name = 2; + + // Region of the resource (non-k8s scope identifier). + string region = 3; + + // CloudAccountIdentifier of the resource (non-k8s scope identifier). + string cloud_account_identifier = 4; +} + +// GetContainerProfileStreamChunk is a single chunk of a streamed +// ContainerProfile download. The first chunk sets metadata + status; +// subsequent chunks set only blob_chunk. +message GetContainerProfileStreamChunk { + // metadata is set on the first chunk only. + GetContainerProfileStreamChunkMetadata metadata = 1; + + // blob_chunk is a slice of the marshaled ContainerProfile payload. + bytes blob_chunk = 2; +} + +// GetContainerProfileStreamChunkMetadata is the status header sent on the +// first chunk of a GetContainerProfileStream response. If success is +// false or exists is false the server closes the stream without sending +// blob chunks. +message GetContainerProfileStreamChunkMetadata { + bool success = 1; + string error_message = 2; + ErrorCode error_code = 3; + + // exists indicates whether a ContainerProfile row was found. + bool exists = 4; +} + // PutSBOMResponse indicates success or failure of the operation. S3 + // Postgres persistence happens asynchronously via Pulsar; success here // means the upload was accepted and the Pulsar message was published. diff --git a/pkg/client/v1/proto/storage_service_grpc.pb.go b/pkg/client/v1/proto/storage_service_grpc.pb.go index 545b62d..cc6a00b 100644 --- a/pkg/client/v1/proto/storage_service_grpc.pb.go +++ b/pkg/client/v1/proto/storage_service_grpc.pb.go @@ -19,12 +19,14 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - StorageService_SendContainerProfile_FullMethodName = "/storageserver.v1.StorageService/SendContainerProfile" - StorageService_GetProfile_FullMethodName = "/storageserver.v1.StorageService/GetProfile" - StorageService_ListApplicationProfiles_FullMethodName = "/storageserver.v1.StorageService/ListApplicationProfiles" - StorageService_ListNetworkNeighborhoods_FullMethodName = "/storageserver.v1.StorageService/ListNetworkNeighborhoods" - StorageService_PutSBOM_FullMethodName = "/storageserver.v1.StorageService/PutSBOM" - StorageService_GetSBOM_FullMethodName = "/storageserver.v1.StorageService/GetSBOM" + StorageService_SendContainerProfile_FullMethodName = "/storageserver.v1.StorageService/SendContainerProfile" + StorageService_GetProfile_FullMethodName = "/storageserver.v1.StorageService/GetProfile" + StorageService_ListApplicationProfiles_FullMethodName = "/storageserver.v1.StorageService/ListApplicationProfiles" + StorageService_ListNetworkNeighborhoods_FullMethodName = "/storageserver.v1.StorageService/ListNetworkNeighborhoods" + StorageService_SendContainerProfileStream_FullMethodName = "/storageserver.v1.StorageService/SendContainerProfileStream" + StorageService_GetContainerProfileStream_FullMethodName = "/storageserver.v1.StorageService/GetContainerProfileStream" + StorageService_PutSBOMStream_FullMethodName = "/storageserver.v1.StorageService/PutSBOMStream" + StorageService_GetSBOMStream_FullMethodName = "/storageserver.v1.StorageService/GetSBOMStream" ) // StorageServiceClient is the client API for StorageService service. @@ -33,19 +35,48 @@ const ( // // StorageService provides gRPC endpoints for node agent to communicate with backend storage type StorageServiceClient interface { + // Deprecated: Do not use. // SendContainerProfile receives a container profile (time-series snapshot) from node agent - // and sends it to Pulsar for processing by the ingester + // and sends it to Pulsar for processing by the ingester. + // + // Deprecated: use SendContainerProfileStream. The unary form is silently + // capped by gRPC's default 4 MiB message limit; container profiles with + // many or large entries (or with MaxContainerProfileSize raised) can + // exceed this and fail on the wire. The streaming variant has no such + // bound. SendContainerProfile(ctx context.Context, in *SendContainerProfileRequest, opts ...grpc.CallOption) (*SendContainerProfileResponse, error) // GetProfile retrieves an aggregated profile (ApplicationProfile, NetworkNeighborhood, - // or ContainerProfile) by fetching them from S3 + // or ContainerProfile) by fetching them from S3. + // + // Note: for ContainerProfile specifically, prefer GetContainerProfileStream, + // which is not subject to the default 4 MiB unary gRPC message limit. GetProfile(ctx context.Context, in *GetProfileRequest, opts ...grpc.CallOption) (*GetProfileResponse, error) // ListApplicationProfiles lists all ApplicationProfiles in a namespace (returns metadata only, nil Spec) ListApplicationProfiles(ctx context.Context, in *ListApplicationProfilesRequest, opts ...grpc.CallOption) (*ListApplicationProfilesResponse, error) // ListNetworkNeighborhoods lists all NetworkNeighborhoods in a namespace (returns metadata only, nil Spec) ListNetworkNeighborhoods(ctx context.Context, in *ListNetworkNeighborhoodsRequest, opts ...grpc.CallOption) (*ListNetworkNeighborhoodsResponse, error) - // PutSBOM uploads an SBOM produced for an image. Client-streaming so the - // caller need not know the payload size in advance and can stream bytes - // from any io.Reader. Idempotent on (customer_guid, image_digest, + // SendContainerProfileStream is the streaming replacement for + // SendContainerProfile. Client-streaming so the caller need not know the + // payload size in advance — required because container profiles with + // many or large entries can exceed the default 4 MiB unary gRPC limit. + // + // The first chunk MUST set metadata (which carries identifying labels + // and may be empty otherwise) and MAY include blob_chunk for small + // payloads. Subsequent chunks set only blob_chunk. Concatenated + // blob_chunk values form the marshaled ContainerProfile proto payload. + SendContainerProfileStream(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[ContainerProfileChunk, SendContainerProfileResponse], error) + // GetContainerProfileStream is the streaming replacement for + // GetProfile(kind=ContainerProfile). Server-streaming so the caller need + // not know the response size in advance. + // + // The first chunk in the response carries metadata + status. When the + // profile does not exist or success is false, the server closes the + // stream after the metadata chunk. Otherwise subsequent chunks carry + // the marshaled ContainerProfile bytes. + GetContainerProfileStream(ctx context.Context, in *GetContainerProfileStreamRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GetContainerProfileStreamChunk], error) + // PutSBOMStream uploads an SBOM produced for an image. Client-streaming + // so the caller need not know the payload size in advance and can stream + // bytes from any io.Reader. Idempotent on (customer_guid, image_digest, // syft_version) — duplicate calls upsert the metadata row and overwrite // the S3 blob; end-state is consistent. // @@ -53,16 +84,16 @@ type StorageServiceClient interface { // and MAY include blob_chunk for small payloads. Subsequent chunks set // only blob_chunk. Concatenated blob_chunk values form the marshaled // SBOMSyft proto payload. - PutSBOM(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[PutSBOMChunk, PutSBOMResponse], error) - // GetSBOM probes for or fetches an SBOM by (customer_guid, image_digest, - // syft_version). Server-streaming so the caller need not know the - // response size in advance. + PutSBOMStream(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[PutSBOMChunk, PutSBOMResponse], error) + // GetSBOMStream probes for or fetches an SBOM by (customer_guid, + // image_digest, syft_version). Server-streaming so the caller need not + // know the response size in advance. // // The first chunk in the response carries metadata + status. When the // request sets metadata_only=true OR the row does not exist OR success // is false, the server closes the stream after the metadata chunk. // Otherwise subsequent chunks carry the marshaled SBOMSyft bytes. - GetSBOM(ctx context.Context, in *GetSBOMRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GetSBOMChunk], error) + GetSBOMStream(ctx context.Context, in *GetSBOMRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GetSBOMChunk], error) } type storageServiceClient struct { @@ -73,6 +104,7 @@ func NewStorageServiceClient(cc grpc.ClientConnInterface) StorageServiceClient { return &storageServiceClient{cc} } +// Deprecated: Do not use. func (c *storageServiceClient) SendContainerProfile(ctx context.Context, in *SendContainerProfileRequest, opts ...grpc.CallOption) (*SendContainerProfileResponse, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(SendContainerProfileResponse) @@ -113,9 +145,41 @@ func (c *storageServiceClient) ListNetworkNeighborhoods(ctx context.Context, in return out, nil } -func (c *storageServiceClient) PutSBOM(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[PutSBOMChunk, PutSBOMResponse], error) { +func (c *storageServiceClient) SendContainerProfileStream(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[ContainerProfileChunk, SendContainerProfileResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StorageService_ServiceDesc.Streams[0], StorageService_SendContainerProfileStream_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[ContainerProfileChunk, SendContainerProfileResponse]{ClientStream: stream} + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StorageService_SendContainerProfileStreamClient = grpc.ClientStreamingClient[ContainerProfileChunk, SendContainerProfileResponse] + +func (c *storageServiceClient) GetContainerProfileStream(ctx context.Context, in *GetContainerProfileStreamRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GetContainerProfileStreamChunk], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &StorageService_ServiceDesc.Streams[0], StorageService_PutSBOM_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &StorageService_ServiceDesc.Streams[1], StorageService_GetContainerProfileStream_FullMethodName, cOpts...) + if err != nil { + return nil, err + } + x := &grpc.GenericClientStream[GetContainerProfileStreamRequest, GetContainerProfileStreamChunk]{ClientStream: stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StorageService_GetContainerProfileStreamClient = grpc.ServerStreamingClient[GetContainerProfileStreamChunk] + +func (c *storageServiceClient) PutSBOMStream(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[PutSBOMChunk, PutSBOMResponse], error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + stream, err := c.cc.NewStream(ctx, &StorageService_ServiceDesc.Streams[2], StorageService_PutSBOMStream_FullMethodName, cOpts...) if err != nil { return nil, err } @@ -124,11 +188,11 @@ func (c *storageServiceClient) PutSBOM(ctx context.Context, opts ...grpc.CallOpt } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type StorageService_PutSBOMClient = grpc.ClientStreamingClient[PutSBOMChunk, PutSBOMResponse] +type StorageService_PutSBOMStreamClient = grpc.ClientStreamingClient[PutSBOMChunk, PutSBOMResponse] -func (c *storageServiceClient) GetSBOM(ctx context.Context, in *GetSBOMRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GetSBOMChunk], error) { +func (c *storageServiceClient) GetSBOMStream(ctx context.Context, in *GetSBOMRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[GetSBOMChunk], error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) - stream, err := c.cc.NewStream(ctx, &StorageService_ServiceDesc.Streams[1], StorageService_GetSBOM_FullMethodName, cOpts...) + stream, err := c.cc.NewStream(ctx, &StorageService_ServiceDesc.Streams[3], StorageService_GetSBOMStream_FullMethodName, cOpts...) if err != nil { return nil, err } @@ -143,7 +207,7 @@ func (c *storageServiceClient) GetSBOM(ctx context.Context, in *GetSBOMRequest, } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type StorageService_GetSBOMClient = grpc.ServerStreamingClient[GetSBOMChunk] +type StorageService_GetSBOMStreamClient = grpc.ServerStreamingClient[GetSBOMChunk] // StorageServiceServer is the server API for StorageService service. // All implementations must embed UnimplementedStorageServiceServer @@ -151,19 +215,48 @@ type StorageService_GetSBOMClient = grpc.ServerStreamingClient[GetSBOMChunk] // // StorageService provides gRPC endpoints for node agent to communicate with backend storage type StorageServiceServer interface { + // Deprecated: Do not use. // SendContainerProfile receives a container profile (time-series snapshot) from node agent - // and sends it to Pulsar for processing by the ingester + // and sends it to Pulsar for processing by the ingester. + // + // Deprecated: use SendContainerProfileStream. The unary form is silently + // capped by gRPC's default 4 MiB message limit; container profiles with + // many or large entries (or with MaxContainerProfileSize raised) can + // exceed this and fail on the wire. The streaming variant has no such + // bound. SendContainerProfile(context.Context, *SendContainerProfileRequest) (*SendContainerProfileResponse, error) // GetProfile retrieves an aggregated profile (ApplicationProfile, NetworkNeighborhood, - // or ContainerProfile) by fetching them from S3 + // or ContainerProfile) by fetching them from S3. + // + // Note: for ContainerProfile specifically, prefer GetContainerProfileStream, + // which is not subject to the default 4 MiB unary gRPC message limit. GetProfile(context.Context, *GetProfileRequest) (*GetProfileResponse, error) // ListApplicationProfiles lists all ApplicationProfiles in a namespace (returns metadata only, nil Spec) ListApplicationProfiles(context.Context, *ListApplicationProfilesRequest) (*ListApplicationProfilesResponse, error) // ListNetworkNeighborhoods lists all NetworkNeighborhoods in a namespace (returns metadata only, nil Spec) ListNetworkNeighborhoods(context.Context, *ListNetworkNeighborhoodsRequest) (*ListNetworkNeighborhoodsResponse, error) - // PutSBOM uploads an SBOM produced for an image. Client-streaming so the - // caller need not know the payload size in advance and can stream bytes - // from any io.Reader. Idempotent on (customer_guid, image_digest, + // SendContainerProfileStream is the streaming replacement for + // SendContainerProfile. Client-streaming so the caller need not know the + // payload size in advance — required because container profiles with + // many or large entries can exceed the default 4 MiB unary gRPC limit. + // + // The first chunk MUST set metadata (which carries identifying labels + // and may be empty otherwise) and MAY include blob_chunk for small + // payloads. Subsequent chunks set only blob_chunk. Concatenated + // blob_chunk values form the marshaled ContainerProfile proto payload. + SendContainerProfileStream(grpc.ClientStreamingServer[ContainerProfileChunk, SendContainerProfileResponse]) error + // GetContainerProfileStream is the streaming replacement for + // GetProfile(kind=ContainerProfile). Server-streaming so the caller need + // not know the response size in advance. + // + // The first chunk in the response carries metadata + status. When the + // profile does not exist or success is false, the server closes the + // stream after the metadata chunk. Otherwise subsequent chunks carry + // the marshaled ContainerProfile bytes. + GetContainerProfileStream(*GetContainerProfileStreamRequest, grpc.ServerStreamingServer[GetContainerProfileStreamChunk]) error + // PutSBOMStream uploads an SBOM produced for an image. Client-streaming + // so the caller need not know the payload size in advance and can stream + // bytes from any io.Reader. Idempotent on (customer_guid, image_digest, // syft_version) — duplicate calls upsert the metadata row and overwrite // the S3 blob; end-state is consistent. // @@ -171,16 +264,16 @@ type StorageServiceServer interface { // and MAY include blob_chunk for small payloads. Subsequent chunks set // only blob_chunk. Concatenated blob_chunk values form the marshaled // SBOMSyft proto payload. - PutSBOM(grpc.ClientStreamingServer[PutSBOMChunk, PutSBOMResponse]) error - // GetSBOM probes for or fetches an SBOM by (customer_guid, image_digest, - // syft_version). Server-streaming so the caller need not know the - // response size in advance. + PutSBOMStream(grpc.ClientStreamingServer[PutSBOMChunk, PutSBOMResponse]) error + // GetSBOMStream probes for or fetches an SBOM by (customer_guid, + // image_digest, syft_version). Server-streaming so the caller need not + // know the response size in advance. // // The first chunk in the response carries metadata + status. When the // request sets metadata_only=true OR the row does not exist OR success // is false, the server closes the stream after the metadata chunk. // Otherwise subsequent chunks carry the marshaled SBOMSyft bytes. - GetSBOM(*GetSBOMRequest, grpc.ServerStreamingServer[GetSBOMChunk]) error + GetSBOMStream(*GetSBOMRequest, grpc.ServerStreamingServer[GetSBOMChunk]) error mustEmbedUnimplementedStorageServiceServer() } @@ -203,11 +296,17 @@ func (UnimplementedStorageServiceServer) ListApplicationProfiles(context.Context func (UnimplementedStorageServiceServer) ListNetworkNeighborhoods(context.Context, *ListNetworkNeighborhoodsRequest) (*ListNetworkNeighborhoodsResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method ListNetworkNeighborhoods not implemented") } -func (UnimplementedStorageServiceServer) PutSBOM(grpc.ClientStreamingServer[PutSBOMChunk, PutSBOMResponse]) error { - return status.Errorf(codes.Unimplemented, "method PutSBOM not implemented") +func (UnimplementedStorageServiceServer) SendContainerProfileStream(grpc.ClientStreamingServer[ContainerProfileChunk, SendContainerProfileResponse]) error { + return status.Errorf(codes.Unimplemented, "method SendContainerProfileStream not implemented") } -func (UnimplementedStorageServiceServer) GetSBOM(*GetSBOMRequest, grpc.ServerStreamingServer[GetSBOMChunk]) error { - return status.Errorf(codes.Unimplemented, "method GetSBOM not implemented") +func (UnimplementedStorageServiceServer) GetContainerProfileStream(*GetContainerProfileStreamRequest, grpc.ServerStreamingServer[GetContainerProfileStreamChunk]) error { + return status.Errorf(codes.Unimplemented, "method GetContainerProfileStream not implemented") +} +func (UnimplementedStorageServiceServer) PutSBOMStream(grpc.ClientStreamingServer[PutSBOMChunk, PutSBOMResponse]) error { + return status.Errorf(codes.Unimplemented, "method PutSBOMStream not implemented") +} +func (UnimplementedStorageServiceServer) GetSBOMStream(*GetSBOMRequest, grpc.ServerStreamingServer[GetSBOMChunk]) error { + return status.Errorf(codes.Unimplemented, "method GetSBOMStream not implemented") } func (UnimplementedStorageServiceServer) mustEmbedUnimplementedStorageServiceServer() {} func (UnimplementedStorageServiceServer) testEmbeddedByValue() {} @@ -302,23 +401,41 @@ func _StorageService_ListNetworkNeighborhoods_Handler(srv interface{}, ctx conte return interceptor(ctx, in, info, handler) } -func _StorageService_PutSBOM_Handler(srv interface{}, stream grpc.ServerStream) error { - return srv.(StorageServiceServer).PutSBOM(&grpc.GenericServerStream[PutSBOMChunk, PutSBOMResponse]{ServerStream: stream}) +func _StorageService_SendContainerProfileStream_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(StorageServiceServer).SendContainerProfileStream(&grpc.GenericServerStream[ContainerProfileChunk, SendContainerProfileResponse]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StorageService_SendContainerProfileStreamServer = grpc.ClientStreamingServer[ContainerProfileChunk, SendContainerProfileResponse] + +func _StorageService_GetContainerProfileStream_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(GetContainerProfileStreamRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(StorageServiceServer).GetContainerProfileStream(m, &grpc.GenericServerStream[GetContainerProfileStreamRequest, GetContainerProfileStreamChunk]{ServerStream: stream}) +} + +// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. +type StorageService_GetContainerProfileStreamServer = grpc.ServerStreamingServer[GetContainerProfileStreamChunk] + +func _StorageService_PutSBOMStream_Handler(srv interface{}, stream grpc.ServerStream) error { + return srv.(StorageServiceServer).PutSBOMStream(&grpc.GenericServerStream[PutSBOMChunk, PutSBOMResponse]{ServerStream: stream}) } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type StorageService_PutSBOMServer = grpc.ClientStreamingServer[PutSBOMChunk, PutSBOMResponse] +type StorageService_PutSBOMStreamServer = grpc.ClientStreamingServer[PutSBOMChunk, PutSBOMResponse] -func _StorageService_GetSBOM_Handler(srv interface{}, stream grpc.ServerStream) error { +func _StorageService_GetSBOMStream_Handler(srv interface{}, stream grpc.ServerStream) error { m := new(GetSBOMRequest) if err := stream.RecvMsg(m); err != nil { return err } - return srv.(StorageServiceServer).GetSBOM(m, &grpc.GenericServerStream[GetSBOMRequest, GetSBOMChunk]{ServerStream: stream}) + return srv.(StorageServiceServer).GetSBOMStream(m, &grpc.GenericServerStream[GetSBOMRequest, GetSBOMChunk]{ServerStream: stream}) } // This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name. -type StorageService_GetSBOMServer = grpc.ServerStreamingServer[GetSBOMChunk] +type StorageService_GetSBOMStreamServer = grpc.ServerStreamingServer[GetSBOMChunk] // StorageService_ServiceDesc is the grpc.ServiceDesc for StorageService service. // It's only intended for direct use with grpc.RegisterService, @@ -346,13 +463,23 @@ var StorageService_ServiceDesc = grpc.ServiceDesc{ }, Streams: []grpc.StreamDesc{ { - StreamName: "PutSBOM", - Handler: _StorageService_PutSBOM_Handler, + StreamName: "SendContainerProfileStream", + Handler: _StorageService_SendContainerProfileStream_Handler, + ClientStreams: true, + }, + { + StreamName: "GetContainerProfileStream", + Handler: _StorageService_GetContainerProfileStream_Handler, + ServerStreams: true, + }, + { + StreamName: "PutSBOMStream", + Handler: _StorageService_PutSBOMStream_Handler, ClientStreams: true, }, { - StreamName: "GetSBOM", - Handler: _StorageService_GetSBOM_Handler, + StreamName: "GetSBOMStream", + Handler: _StorageService_GetSBOMStream_Handler, ServerStreams: true, }, }, diff --git a/pkg/client/v1/storageclient.go b/pkg/client/v1/storageclient.go index 74c28a4..f518ffb 100644 --- a/pkg/client/v1/storageclient.go +++ b/pkg/client/v1/storageclient.go @@ -248,7 +248,12 @@ func (c *StorageClient) withMetadata(ctx context.Context) context.Context { return metadata.NewOutgoingContext(ctx, c.metadata) } -// SendContainerProfile sends a container profile to the storage server +// SendContainerProfile sends a container profile to the storage server. +// +// Deprecated: use SendContainerProfileStream. The unary form is silently +// capped at gRPC's default 4 MiB message size; profiles with many or +// large entries can exceed this and fail on the wire. The streaming +// variant has no such bound. func (c *StorageClient) SendContainerProfile(ctx context.Context, profile *v1beta1.ContainerProfile) (*proto.SendContainerProfileResponse, error) { if c.protoClient == nil { return nil, fmt.Errorf("client is not connected") @@ -269,6 +274,135 @@ func (c *StorageClient) SendContainerProfile(ctx context.Context, profile *v1bet return c.protoClient.SendContainerProfile(ctx, req) } +// SendContainerProfileStream is the streaming replacement for +// SendContainerProfile. The profile is marshaled and sent to the server +// in chunks of sbomStreamChunkSize. Use this whenever you might write a +// large container profile (many entries, long stack traces, long paths). +func (c *StorageClient) SendContainerProfileStream(ctx context.Context, profile *v1beta1.ContainerProfile) (*proto.SendContainerProfileResponse, error) { + if c.protoClient == nil { + return nil, fmt.Errorf("client is not connected") + } + if profile == nil { + return nil, fmt.Errorf("profile is nil") + } + + payload, err := profile.Marshal() + if err != nil { + return nil, fmt.Errorf("failed to marshal ContainerProfile: %w", err) + } + + ctx = c.withMetadata(ctx) + + if c.callTimeout != nil && *c.callTimeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *c.callTimeout) + defer cancel() + } + + stream, err := c.protoClient.SendContainerProfileStream(ctx) + if err != nil { + return nil, fmt.Errorf("failed to open SendContainerProfileStream: %w", err) + } + + // First chunk MUST set metadata. The metadata message is currently empty + // (reserved for future per-call options); identifying fields continue to + // be carried via gRPC metadata headers. + cut := sbomStreamChunkSize + if cut > len(payload) { + cut = len(payload) + } + first := &proto.ContainerProfileChunk{ + Metadata: &proto.ContainerProfileChunkMetadata{}, + BlobChunk: payload[:cut], + } + if err := stream.Send(first); err != nil { + return nil, fmt.Errorf("failed to send first chunk: %w", err) + } + + for offset := cut; offset < len(payload); offset += sbomStreamChunkSize { + end := offset + sbomStreamChunkSize + if end > len(payload) { + end = len(payload) + } + if err := stream.Send(&proto.ContainerProfileChunk{BlobChunk: payload[offset:end]}); err != nil { + return nil, fmt.Errorf("failed to send chunk: %w", err) + } + } + + return stream.CloseAndRecv() +} + +// GetContainerProfileStream is the streaming replacement for the +// ContainerProfile branch of GetProfile. Use whenever the profile may +// exceed the default 4 MiB unary gRPC message limit. The chunks are +// reassembled and the marshaled bytes are unmarshaled internally — the +// caller receives a typed *v1beta1.ContainerProfile just as with the +// existing GetContainerProfile wrapper. +func (c *StorageClient) GetContainerProfileStream(ctx context.Context, namespace, name string, opts ...ProfileOption) (*v1beta1.ContainerProfile, error) { + if c.protoClient == nil { + return nil, fmt.Errorf("client is not connected") + } + + profileOpts := profileOptionsWithDefaults(opts) + + req := &proto.GetContainerProfileStreamRequest{ + Namespace: namespace, + Name: name, + Region: profileOpts.Region, + CloudAccountIdentifier: profileOpts.CloudAccountIdentifier, + } + + ctx = c.withMetadata(ctx) + + if c.callTimeout != nil && *c.callTimeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(ctx, *c.callTimeout) + defer cancel() + } + + stream, err := c.protoClient.GetContainerProfileStream(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to open GetContainerProfileStream: %w", err) + } + + // First chunk MUST carry metadata. + firstChunk, err := stream.Recv() + if err != nil { + return nil, fmt.Errorf("failed to receive first chunk: %w", err) + } + md := firstChunk.GetMetadata() + if md == nil { + return nil, fmt.Errorf("first GetContainerProfileStream chunk missing metadata") + } + if !md.Success { + return nil, fmt.Errorf("failed to get container profile: %s (code: %v)", md.ErrorMessage, md.ErrorCode) + } + if !md.Exists { + return nil, fmt.Errorf("container profile %s/%s not found", namespace, name) + } + + var buf []byte + if len(firstChunk.BlobChunk) > 0 { + buf = append(buf, firstChunk.BlobChunk...) + } + for { + chunk, err := stream.Recv() + if err == io.EOF { + break + } + if err != nil { + return nil, fmt.Errorf("failed to receive chunk: %w", err) + } + buf = append(buf, chunk.BlobChunk...) + } + + profile := &v1beta1.ContainerProfile{} + if err := profile.Unmarshal(buf); err != nil { + return nil, fmt.Errorf("failed to unmarshal ContainerProfile: %w", err) + } + return profile, nil +} + // GetApplicationProfile retrieves an aggregated ApplicationProfile from the storage server // For backward compatibility, region and cloudAccountIdentifier can be provided via ProfileOption // Old way: GetApplicationProfile(ctx, "ns", "name") @@ -494,14 +628,14 @@ func (c *StorageClient) ListNetworkNeighborhoods(ctx context.Context, namespace return list, nil } -// PutSBOM uploads an SBOM identified by (image_digest, syft_version, +// PutSBOMStream uploads an SBOM identified by (image_digest, syft_version, // source). The payload is the marshaled SBOMSyft proto, read from r and // sent to the server in chunks of sbomStreamChunkSize. Callers with a // typed *v1beta1.SBOMSyft should use MarshalSBOM to obtain r. // // The underlying RPC is client-streaming; the caller never needs to know // the payload size in advance. -func (c *StorageClient) PutSBOM(ctx context.Context, imageDigest, syftVersion string, source proto.SBOMSource, r io.Reader) (*proto.PutSBOMResponse, error) { +func (c *StorageClient) PutSBOMStream(ctx context.Context, imageDigest, syftVersion string, source proto.SBOMSource, r io.Reader) (*proto.PutSBOMResponse, error) { if c.protoClient == nil { return nil, fmt.Errorf("client is not connected") } @@ -517,9 +651,9 @@ func (c *StorageClient) PutSBOM(ctx context.Context, imageDigest, syftVersion st defer cancel() } - stream, err := c.protoClient.PutSBOM(ctx) + stream, err := c.protoClient.PutSBOMStream(ctx) if err != nil { - return nil, fmt.Errorf("failed to open PutSBOM stream: %w", err) + return nil, fmt.Errorf("failed to open PutSBOMStream: %w", err) } // First chunk MUST set metadata, and MAY carry the first slice of bytes. @@ -558,7 +692,7 @@ func (c *StorageClient) PutSBOM(ctx context.Context, imageDigest, syftVersion st return stream.CloseAndRecv() } -// GetSBOM probes for or fetches an SBOM by (image_digest, syft_version). +// GetSBOMStream probes for or fetches an SBOM by (image_digest, syft_version). // // When metadataOnly is true, or the row does not exist, or the server // reports a non-success status, the returned io.ReadCloser is nil — the @@ -571,7 +705,7 @@ func (c *StorageClient) PutSBOM(ctx context.Context, imageDigest, syftVersion st // // The underlying RPC is server-streaming; the caller never needs to know // the response size in advance. -func (c *StorageClient) GetSBOM(ctx context.Context, imageDigest, syftVersion string, metadataOnly bool) (*proto.GetSBOMChunkMetadata, io.ReadCloser, error) { +func (c *StorageClient) GetSBOMStream(ctx context.Context, imageDigest, syftVersion string, metadataOnly bool) (*proto.GetSBOMChunkMetadata, io.ReadCloser, error) { if c.protoClient == nil { return nil, nil, fmt.Errorf("client is not connected") } @@ -589,10 +723,10 @@ func (c *StorageClient) GetSBOM(ctx context.Context, imageDigest, syftVersion st // per-call timeout would clip large downloads. Callers wanting a // timeout should pass a ctx with their own deadline. streamCtx, cancel := context.WithCancel(ctx) - stream, err := c.protoClient.GetSBOM(streamCtx, req) + stream, err := c.protoClient.GetSBOMStream(streamCtx, req) if err != nil { cancel() - return nil, nil, fmt.Errorf("failed to open GetSBOM stream: %w", err) + return nil, nil, fmt.Errorf("failed to open GetSBOMStream: %w", err) } // First chunk MUST carry metadata. @@ -604,7 +738,7 @@ func (c *StorageClient) GetSBOM(ctx context.Context, imageDigest, syftVersion st md := firstChunk.GetMetadata() if md == nil { cancel() - return nil, nil, fmt.Errorf("first GetSBOM chunk missing metadata") + return nil, nil, fmt.Errorf("first GetSBOMStream chunk missing metadata") } // On miss, error, or metadata-only request, the server closes the diff --git a/pkg/client/v1/storageclient_test.go b/pkg/client/v1/storageclient_test.go index 43dcb47..0304f89 100644 --- a/pkg/client/v1/storageclient_test.go +++ b/pkg/client/v1/storageclient_test.go @@ -26,8 +26,10 @@ type mockStorageServiceClient struct { getProfileFunc func(ctx context.Context, in *proto.GetProfileRequest, opts ...grpc.CallOption) (*proto.GetProfileResponse, error) listApplicationProfilesFunc func(ctx context.Context, in *proto.ListApplicationProfilesRequest, opts ...grpc.CallOption) (*proto.ListApplicationProfilesResponse, error) listNetworkNeighborhoodsFunc func(ctx context.Context, in *proto.ListNetworkNeighborhoodsRequest, opts ...grpc.CallOption) (*proto.ListNetworkNeighborhoodsResponse, error) - putSBOMFunc func(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[proto.PutSBOMChunk, proto.PutSBOMResponse], error) - getSBOMFunc func(ctx context.Context, in *proto.GetSBOMRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[proto.GetSBOMChunk], error) + putSBOMStreamFunc func(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[proto.PutSBOMChunk, proto.PutSBOMResponse], error) + getSBOMStreamFunc func(ctx context.Context, in *proto.GetSBOMRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[proto.GetSBOMChunk], error) + sendContainerProfileStreamFunc func(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[proto.ContainerProfileChunk, proto.SendContainerProfileResponse], error) + getContainerProfileStreamFunc func(ctx context.Context, in *proto.GetContainerProfileStreamRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[proto.GetContainerProfileStreamChunk], error) } func (m *mockStorageServiceClient) SendContainerProfile(ctx context.Context, in *proto.SendContainerProfileRequest, opts ...grpc.CallOption) (*proto.SendContainerProfileResponse, error) { @@ -58,18 +60,32 @@ func (m *mockStorageServiceClient) ListNetworkNeighborhoods(ctx context.Context, return &proto.ListNetworkNeighborhoodsResponse{Success: true}, nil } -func (m *mockStorageServiceClient) PutSBOM(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[proto.PutSBOMChunk, proto.PutSBOMResponse], error) { - if m.putSBOMFunc != nil { - return m.putSBOMFunc(ctx, opts...) +func (m *mockStorageServiceClient) PutSBOMStream(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[proto.PutSBOMChunk, proto.PutSBOMResponse], error) { + if m.putSBOMStreamFunc != nil { + return m.putSBOMStreamFunc(ctx, opts...) } - return nil, fmt.Errorf("PutSBOM not implemented in mock") + return nil, fmt.Errorf("PutSBOMStream not implemented in mock") } -func (m *mockStorageServiceClient) GetSBOM(ctx context.Context, in *proto.GetSBOMRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[proto.GetSBOMChunk], error) { - if m.getSBOMFunc != nil { - return m.getSBOMFunc(ctx, in, opts...) +func (m *mockStorageServiceClient) GetSBOMStream(ctx context.Context, in *proto.GetSBOMRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[proto.GetSBOMChunk], error) { + if m.getSBOMStreamFunc != nil { + return m.getSBOMStreamFunc(ctx, in, opts...) } - return nil, fmt.Errorf("GetSBOM not implemented in mock") + return nil, fmt.Errorf("GetSBOMStream not implemented in mock") +} + +func (m *mockStorageServiceClient) SendContainerProfileStream(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStreamingClient[proto.ContainerProfileChunk, proto.SendContainerProfileResponse], error) { + if m.sendContainerProfileStreamFunc != nil { + return m.sendContainerProfileStreamFunc(ctx, opts...) + } + return nil, fmt.Errorf("SendContainerProfileStream not implemented in mock") +} + +func (m *mockStorageServiceClient) GetContainerProfileStream(ctx context.Context, in *proto.GetContainerProfileStreamRequest, opts ...grpc.CallOption) (grpc.ServerStreamingClient[proto.GetContainerProfileStreamChunk], error) { + if m.getContainerProfileStreamFunc != nil { + return m.getContainerProfileStreamFunc(ctx, in, opts...) + } + return nil, fmt.Errorf("GetContainerProfileStream not implemented in mock") } func TestNewStorageClient(t *testing.T) { @@ -639,28 +655,39 @@ func TestParseGRPCURL(t *testing.T) { } } -// sbomRoundTripServer is a minimal proto.StorageServiceServer impl that -// records what PutSBOM receives and serves what GetSBOM should return. +// storageRoundTripServer is a minimal proto.StorageServiceServer impl that +// records what PutSBOMStream receives and serves what GetSBOMStream should return. // It exercises the full marshal/unmarshal path of the streaming RPCs // end-to-end through bufconn — catching wire-shape bugs the prior // mock-based tests would miss. -type sbomRoundTripServer struct { +type storageRoundTripServer struct { proto.UnimplementedStorageServiceServer mu sync.Mutex - // Received state from the most recent PutSBOM call. + // SBOM upload state: captured from the most recent PutSBOMStream call. receivedMetadata *proto.PutSBOMChunkMetadata receivedBytes []byte - // Configured response for GetSBOM. If exists is false the server - // closes the stream after the metadata chunk. + // SBOM download config: returned from the next GetSBOMStream call. + // If serveExists is false the server closes the stream after the + // metadata chunk. serveExists bool serveMetadata *proto.SBOMMetadata serveBytes []byte serveChunkSize int // 0 → send the whole payload in one chunk + + // ContainerProfile upload state: captured from the most recent + // SendContainerProfileStream call. + cpReceivedBytes []byte + + // ContainerProfile download config: returned from the next + // GetContainerProfileStream call. + cpServeExists bool + cpServeBytes []byte + cpServeChunkSize int } -func (s *sbomRoundTripServer) PutSBOM(stream grpc.ClientStreamingServer[proto.PutSBOMChunk, proto.PutSBOMResponse]) error { +func (s *storageRoundTripServer) PutSBOMStream(stream grpc.ClientStreamingServer[proto.PutSBOMChunk, proto.PutSBOMResponse]) error { var buf []byte for { chunk, err := stream.Recv() @@ -683,7 +710,7 @@ func (s *sbomRoundTripServer) PutSBOM(stream grpc.ClientStreamingServer[proto.Pu return stream.SendAndClose(&proto.PutSBOMResponse{Success: true}) } -func (s *sbomRoundTripServer) GetSBOM(req *proto.GetSBOMRequest, stream grpc.ServerStreamingServer[proto.GetSBOMChunk]) error { +func (s *storageRoundTripServer) GetSBOMStream(req *proto.GetSBOMRequest, stream grpc.ServerStreamingServer[proto.GetSBOMChunk]) error { s.mu.Lock() exists := s.serveExists metadata := s.serveMetadata @@ -722,14 +749,66 @@ func (s *sbomRoundTripServer) GetSBOM(req *proto.GetSBOMRequest, stream grpc.Ser return nil } -// startBufconnSBOMServer starts the round-trip server on an in-memory +func (s *storageRoundTripServer) SendContainerProfileStream(stream grpc.ClientStreamingServer[proto.ContainerProfileChunk, proto.SendContainerProfileResponse]) error { + var buf []byte + for { + chunk, err := stream.Recv() + if err == io.EOF { + break + } + if err != nil { + return err + } + buf = append(buf, chunk.BlobChunk...) + } + s.mu.Lock() + s.cpReceivedBytes = buf + s.mu.Unlock() + return stream.SendAndClose(&proto.SendContainerProfileResponse{Success: true}) +} + +func (s *storageRoundTripServer) GetContainerProfileStream(req *proto.GetContainerProfileStreamRequest, stream grpc.ServerStreamingServer[proto.GetContainerProfileStreamChunk]) error { + s.mu.Lock() + exists := s.cpServeExists + payload := s.cpServeBytes + chunkSize := s.cpServeChunkSize + s.mu.Unlock() + + first := &proto.GetContainerProfileStreamChunk{ + Metadata: &proto.GetContainerProfileStreamChunkMetadata{ + Success: true, + Exists: exists, + }, + } + if err := stream.Send(first); err != nil { + return err + } + if !exists { + return nil + } + if chunkSize <= 0 || chunkSize >= len(payload) { + return stream.Send(&proto.GetContainerProfileStreamChunk{BlobChunk: payload}) + } + for offset := 0; offset < len(payload); offset += chunkSize { + end := offset + chunkSize + if end > len(payload) { + end = len(payload) + } + if err := stream.Send(&proto.GetContainerProfileStreamChunk{BlobChunk: payload[offset:end]}); err != nil { + return err + } + } + return nil +} + +// startBufconnStorageServer starts the round-trip server on an in-memory // bufconn listener and returns a client connected to it. -func startBufconnSBOMServer(t *testing.T) (*sbomRoundTripServer, *StorageClient, func()) { +func startBufconnStorageServer(t *testing.T) (*storageRoundTripServer, *StorageClient, func()) { t.Helper() const bufsize = 1024 * 1024 lis := bufconn.Listen(bufsize) srv := grpc.NewServer() - rtSrv := &sbomRoundTripServer{} + rtSrv := &storageRoundTripServer{} proto.RegisterStorageServiceServer(srv, rtSrv) go func() { _ = srv.Serve(lis) }() @@ -800,14 +879,14 @@ func TestStorageClient_SBOMRoundTrip(t *testing.T) { source := proto.SBOMSource_SBOM_SOURCE_WORKLOAD t.Run("PutSBOM marshals through MarshalSBOM and arrives intact", func(t *testing.T) { - rtSrv, client, cleanup := startBufconnSBOMServer(t) + rtSrv, client, cleanup := startBufconnStorageServer(t) defer cleanup() original := sampleSBOMSyft() reader, err := MarshalSBOM(original) require.NoError(t, err) - resp, err := client.PutSBOM(context.Background(), imageDigest, syftVersion, source, reader) + resp, err := client.PutSBOMStream(context.Background(), imageDigest, syftVersion, source, reader) require.NoError(t, err) assert.True(t, resp.Success) @@ -829,7 +908,7 @@ func TestStorageClient_SBOMRoundTrip(t *testing.T) { }) t.Run("GetSBOM metadata-only probe returns only metadata", func(t *testing.T) { - rtSrv, client, cleanup := startBufconnSBOMServer(t) + rtSrv, client, cleanup := startBufconnStorageServer(t) defer cleanup() rtSrv.serveExists = true @@ -838,7 +917,7 @@ func TestStorageClient_SBOMRoundTrip(t *testing.T) { SyftVersion: syftVersion, } - md, reader, err := client.GetSBOM(context.Background(), imageDigest, syftVersion, true) + md, reader, err := client.GetSBOMStream(context.Background(), imageDigest, syftVersion, true) require.NoError(t, err) assert.True(t, md.Success) assert.True(t, md.Exists) @@ -848,12 +927,12 @@ func TestStorageClient_SBOMRoundTrip(t *testing.T) { }) t.Run("GetSBOM probe miss returns exists=false and no reader", func(t *testing.T) { - rtSrv, client, cleanup := startBufconnSBOMServer(t) + rtSrv, client, cleanup := startBufconnStorageServer(t) defer cleanup() rtSrv.serveExists = false - md, reader, err := client.GetSBOM(context.Background(), imageDigest, syftVersion, true) + md, reader, err := client.GetSBOMStream(context.Background(), imageDigest, syftVersion, true) require.NoError(t, err) assert.True(t, md.Success) assert.False(t, md.Exists) @@ -861,7 +940,7 @@ func TestStorageClient_SBOMRoundTrip(t *testing.T) { }) t.Run("GetSBOM full fetch round-trips through UnmarshalSBOM (single chunk)", func(t *testing.T) { - rtSrv, client, cleanup := startBufconnSBOMServer(t) + rtSrv, client, cleanup := startBufconnStorageServer(t) defer cleanup() original := sampleSBOMSyft() @@ -872,7 +951,7 @@ func TestStorageClient_SBOMRoundTrip(t *testing.T) { rtSrv.serveBytes = payload rtSrv.serveChunkSize = 0 // single chunk - md, reader, err := client.GetSBOM(context.Background(), imageDigest, syftVersion, false) + md, reader, err := client.GetSBOMStream(context.Background(), imageDigest, syftVersion, false) require.NoError(t, err) require.NotNil(t, reader) defer reader.Close() @@ -885,7 +964,7 @@ func TestStorageClient_SBOMRoundTrip(t *testing.T) { }) t.Run("GetSBOM full fetch round-trips with payload split across many chunks", func(t *testing.T) { - rtSrv, client, cleanup := startBufconnSBOMServer(t) + rtSrv, client, cleanup := startBufconnStorageServer(t) defer cleanup() original := sampleSBOMSyft() @@ -898,7 +977,7 @@ func TestStorageClient_SBOMRoundTrip(t *testing.T) { // across reads smaller than a chunk — exercises the buffering logic. rtSrv.serveChunkSize = 7 - md, reader, err := client.GetSBOM(context.Background(), imageDigest, syftVersion, false) + md, reader, err := client.GetSBOMStream(context.Background(), imageDigest, syftVersion, false) require.NoError(t, err) require.NotNil(t, reader) defer reader.Close() @@ -926,10 +1005,10 @@ func TestStorageClient_SBOMRoundTrip(t *testing.T) { // TestStorageClient_PutSBOM_NilReader and the next test cover the trivial // argument-validation paths that the bufconn round-trip doesn't exercise. func TestStorageClient_PutSBOM_NilReader(t *testing.T) { - _, client, cleanup := startBufconnSBOMServer(t) + _, client, cleanup := startBufconnStorageServer(t) defer cleanup() - _, err := client.PutSBOM(context.Background(), "abc", "1.0.0", proto.SBOMSource_SBOM_SOURCE_WORKLOAD, nil) + _, err := client.PutSBOMStream(context.Background(), "abc", "1.0.0", proto.SBOMSource_SBOM_SOURCE_WORKLOAD, nil) require.Error(t, err) assert.Contains(t, err.Error(), "nil") } @@ -943,3 +1022,80 @@ func TestUnmarshalSBOM_NilReader(t *testing.T) { _, err := UnmarshalSBOM(nil) require.Error(t, err) } + +// sampleContainerProfile is the CP counterpart of sampleSBOMSyft: a +// non-trivial ContainerProfile used to verify that the streaming RPCs +// marshal and unmarshal fields correctly end-to-end. +func sampleContainerProfile() *v1beta1.ContainerProfile { + return &v1beta1.ContainerProfile{ + TypeMeta: metav1.TypeMeta{ + Kind: "ContainerProfile", + APIVersion: "spdx.softwarecomposition.kubescape.io/v1beta1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "nginx-7d4f8b9c-abc12", + Namespace: "default", + Labels: map[string]string{ + "app.kubernetes.io/name": "nginx", + }, + }, + } +} + +// TestStorageClient_ContainerProfileStreamRoundTrip mirrors the SBOM +// round-trip test for the new CP streaming RPCs. Stands up a real gRPC +// server over bufconn and verifies the upload + download paths. +func TestStorageClient_ContainerProfileStreamRoundTrip(t *testing.T) { + t.Run("SendContainerProfileStream marshals and arrives intact", func(t *testing.T) { + rtSrv, client, cleanup := startBufconnStorageServer(t) + defer cleanup() + + original := sampleContainerProfile() + resp, err := client.SendContainerProfileStream(context.Background(), original) + require.NoError(t, err) + assert.True(t, resp.Success) + + require.NotEmpty(t, rtSrv.cpReceivedBytes) + got := &v1beta1.ContainerProfile{} + require.NoError(t, got.Unmarshal(rtSrv.cpReceivedBytes)) + assert.Equal(t, original.Name, got.Name) + assert.Equal(t, original.Namespace, got.Namespace) + assert.Equal(t, original.Labels["app.kubernetes.io/name"], got.Labels["app.kubernetes.io/name"]) + }) + + t.Run("GetContainerProfileStream round-trips with payload split across chunks", func(t *testing.T) { + rtSrv, client, cleanup := startBufconnStorageServer(t) + defer cleanup() + + original := sampleContainerProfile() + payload, err := original.Marshal() + require.NoError(t, err) + rtSrv.cpServeExists = true + rtSrv.cpServeBytes = payload + rtSrv.cpServeChunkSize = 7 // small chunks stress the reassembly loop + + got, err := client.GetContainerProfileStream(context.Background(), original.Namespace, original.Name) + require.NoError(t, err) + assert.Equal(t, original.Name, got.Name) + assert.Equal(t, original.Namespace, got.Namespace) + assert.Equal(t, original.Labels["app.kubernetes.io/name"], got.Labels["app.kubernetes.io/name"]) + }) + + t.Run("GetContainerProfileStream returns not-found error when row is absent", func(t *testing.T) { + _, client, cleanup := startBufconnStorageServer(t) + defer cleanup() + + _, err := client.GetContainerProfileStream(context.Background(), "default", "missing") + require.Error(t, err) + assert.Contains(t, err.Error(), "not found") + }) +} + +func TestStorageClient_SendContainerProfileStream_NilProfile(t *testing.T) { + _, client, cleanup := startBufconnStorageServer(t) + defer cleanup() + + _, err := client.SendContainerProfileStream(context.Background(), nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "nil") +} From 67a2c251789d68962271bfa49e52a564422457ce Mon Sep 17 00:00:00 2001 From: jnathangreeg Date: Tue, 12 May 2026 12:04:27 +0300 Subject: [PATCH 4/6] NAUT-1310: also deprecate GetContainerProfile Go wrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It now has a streaming replacement (GetContainerProfileStream) and delegates to the unary GetProfile RPC under the hood, which carries the same 4 MiB cliff exposure. GetApplicationProfile and GetNetworkNeighborhood remain undecorated — they don't have streaming variants yet. The proto-level GetProfile RPC also stays as-is because it still serves AP/NN. Co-Authored-By: Claude Opus 4.7 (1M context) --- pkg/client/v1/storageclient.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/client/v1/storageclient.go b/pkg/client/v1/storageclient.go index f518ffb..d385e4d 100644 --- a/pkg/client/v1/storageclient.go +++ b/pkg/client/v1/storageclient.go @@ -481,7 +481,12 @@ func (c *StorageClient) GetNetworkNeighborhood(ctx context.Context, namespace, n return resp.NetworkNeighborhood, nil } -// GetContainerProfile retrieves a ContainerProfile from the storage server +// GetContainerProfile retrieves a ContainerProfile from the storage server. +// +// Deprecated: use GetContainerProfileStream. The unary form goes through +// GetProfile, which is capped at gRPC's default 4 MiB message size; +// profiles with many or large entries can exceed this and fail on the +// wire. The streaming variant has no such bound. func (c *StorageClient) GetContainerProfile(ctx context.Context, namespace, name string, opts ...ProfileOption) (*v1beta1.ContainerProfile, error) { if c.protoClient == nil { return nil, fmt.Errorf("client is not connected") From 05b150756e33e9154de10fb0ee2768a9aca76ce2 Mon Sep 17 00:00:00 2001 From: Jonathan Green Date: Tue, 12 May 2026 04:28:06 -0700 Subject: [PATCH 5/6] Update pkg/client/v1/storageclient.go Co-authored-by: Matthias Bertschy Signed-off-by: Jonathan Green --- pkg/client/v1/storageclient.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/client/v1/storageclient.go b/pkg/client/v1/storageclient.go index d385e4d..7921692 100644 --- a/pkg/client/v1/storageclient.go +++ b/pkg/client/v1/storageclient.go @@ -650,10 +650,9 @@ func (c *StorageClient) PutSBOMStream(ctx context.Context, imageDigest, syftVers ctx = c.withMetadata(ctx) - if c.callTimeout != nil && *c.callTimeout > 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, *c.callTimeout) - defer cancel() +// Note: we deliberately do NOT apply callTimeout here, because stream +// duration depends on payload size / network speed. Callers wanting a +// timeout should pass a ctx with their own deadline. } stream, err := c.protoClient.PutSBOMStream(ctx) From 1e17d4ce61c105b60db1ab6211467f6ced987135 Mon Sep 17 00:00:00 2001 From: jnathangreeg Date: Tue, 12 May 2026 14:32:54 +0300 Subject: [PATCH 6/6] NAUT-1310: address review on PR #50 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CodeRabbit: GetSBOMStream silently masked server-side failures. When the server reported !md.Success the helper returned (md, nil, nil), so a caller checking `if err != nil` couldn't tell a real RPC failure apart from a clean miss. Now returns a non-nil error on !md.Success, wrapping md.ErrorMessage / md.ErrorCode. !md.Exists and metadataOnly remain the (md, nil, nil) "no reader, not an error" paths. Mirrors GetContainerProfileStream's existing contract; doc updated to lead with the contract. - matthyx (line 656): PutSBOMStream was wrapping the stream context in callTimeout — defeats the purpose of streaming for large payloads. Dropped on PutSBOMStream, SendContainerProfileStream, and GetContainerProfileStream (the latter two had the same bug). GetSBOMStream already avoided it explicitly. All four streaming wrappers now share the same comment explaining why. - matthyx (line 874): added TestStorageClient_GetSBOMStream_NotBounded ByCallTimeout. Configures the client with WithCallTimeout(20ms), has the server delay between chunks by 50ms × 5 chunks = 250ms of server-side stream time (12× the configured timeout). If the stream were wrapped in context.WithTimeout(callTimeout), the call would fail with deadline-exceeded in the first 20ms. Stays under 500ms wall time so it's CI-safe. Extended startBufconnStorageServer to forward StorageClientOption variadics and added a serveChunkDelay knob to the round-trip server. Co-Authored-By: Claude Opus 4.7 (1M context) --- pkg/client/v1/storageclient.go | 61 ++++++++++++++------------- pkg/client/v1/storageclient_test.go | 65 +++++++++++++++++++++++++++-- 2 files changed, 95 insertions(+), 31 deletions(-) diff --git a/pkg/client/v1/storageclient.go b/pkg/client/v1/storageclient.go index 7921692..5bea461 100644 --- a/pkg/client/v1/storageclient.go +++ b/pkg/client/v1/storageclient.go @@ -291,14 +291,12 @@ func (c *StorageClient) SendContainerProfileStream(ctx context.Context, profile return nil, fmt.Errorf("failed to marshal ContainerProfile: %w", err) } + // Note: we deliberately do NOT apply callTimeout here. Stream duration + // depends on payload size / network speed; the per-call timeout was + // chosen for short unary RPCs. Callers wanting a deadline should pass + // a ctx with their own. ctx = c.withMetadata(ctx) - if c.callTimeout != nil && *c.callTimeout > 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, *c.callTimeout) - defer cancel() - } - stream, err := c.protoClient.SendContainerProfileStream(ctx) if err != nil { return nil, fmt.Errorf("failed to open SendContainerProfileStream: %w", err) @@ -352,14 +350,12 @@ func (c *StorageClient) GetContainerProfileStream(ctx context.Context, namespace CloudAccountIdentifier: profileOpts.CloudAccountIdentifier, } + // Note: we deliberately do NOT apply callTimeout here. Stream duration + // depends on payload size / network speed; the per-call timeout was + // chosen for short unary RPCs. Callers wanting a deadline should pass + // a ctx with their own. ctx = c.withMetadata(ctx) - if c.callTimeout != nil && *c.callTimeout > 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(ctx, *c.callTimeout) - defer cancel() - } - stream, err := c.protoClient.GetContainerProfileStream(ctx, req) if err != nil { return nil, fmt.Errorf("failed to open GetContainerProfileStream: %w", err) @@ -648,13 +644,12 @@ func (c *StorageClient) PutSBOMStream(ctx context.Context, imageDigest, syftVers return nil, fmt.Errorf("payload reader is nil") } + // Note: we deliberately do NOT apply callTimeout here. Stream duration + // depends on payload size / network speed; the per-call timeout was + // chosen for short unary RPCs. Callers wanting a deadline should pass + // a ctx with their own. ctx = c.withMetadata(ctx) -// Note: we deliberately do NOT apply callTimeout here, because stream -// duration depends on payload size / network speed. Callers wanting a -// timeout should pass a ctx with their own deadline. - } - stream, err := c.protoClient.PutSBOMStream(ctx) if err != nil { return nil, fmt.Errorf("failed to open PutSBOMStream: %w", err) @@ -698,14 +693,17 @@ func (c *StorageClient) PutSBOMStream(ctx context.Context, imageDigest, syftVers // GetSBOMStream probes for or fetches an SBOM by (image_digest, syft_version). // -// When metadataOnly is true, or the row does not exist, or the server -// reports a non-success status, the returned io.ReadCloser is nil — the -// caller consults the metadata for the answer. -// -// Otherwise the returned io.ReadCloser streams the marshaled SBOMSyft -// proto bytes; use UnmarshalSBOM (or read into your own buffer) to -// reconstruct the typed object. The caller MUST Close the reader to -// release the underlying gRPC stream. +// Contract: +// - Server-reported failure → returns a non-nil error along with the +// metadata. Caller's `if err != nil { ... }` is enough; no need to +// inspect metadata.Success separately. +// - Row does not exist OR metadataOnly is true → returns (md, nil, nil). +// Caller consults `md.Exists` to distinguish probe-hit from miss. +// - Row exists and metadataOnly is false → returns (md, reader, nil). +// The reader streams the marshaled SBOMSyft proto bytes; use +// UnmarshalSBOM (or read into your own buffer) to reconstruct the +// typed object. The caller MUST Close the reader to release the +// underlying gRPC stream. // // The underlying RPC is server-streaming; the caller never needs to know // the response size in advance. @@ -745,9 +743,16 @@ func (c *StorageClient) GetSBOMStream(ctx context.Context, imageDigest, syftVers return nil, nil, fmt.Errorf("first GetSBOMStream chunk missing metadata") } - // On miss, error, or metadata-only request, the server closes the - // stream after the metadata chunk. Drain and return nil reader. - if !md.Success || !md.Exists || metadataOnly { + // A server-side failure is a real error — surface it instead of letting + // callers misread (md, nil, nil) as a clean miss. + if !md.Success { + cancel() + return md, nil, fmt.Errorf("server reported failure: %s (code: %v)", md.ErrorMessage, md.ErrorCode) + } + + // On miss or metadata-only request, the server closes the stream after + // the metadata chunk. Drain and return nil reader. + if !md.Exists || metadataOnly { cancel() return md, nil, nil } diff --git a/pkg/client/v1/storageclient_test.go b/pkg/client/v1/storageclient_test.go index 0304f89..34f684e 100644 --- a/pkg/client/v1/storageclient_test.go +++ b/pkg/client/v1/storageclient_test.go @@ -675,6 +675,7 @@ type storageRoundTripServer struct { serveMetadata *proto.SBOMMetadata serveBytes []byte serveChunkSize int // 0 → send the whole payload in one chunk + serveChunkDelay time.Duration // sleep before each chunk; for timeout tests // ContainerProfile upload state: captured from the most recent // SendContainerProfileStream call. @@ -716,6 +717,7 @@ func (s *storageRoundTripServer) GetSBOMStream(req *proto.GetSBOMRequest, stream metadata := s.serveMetadata payload := s.serveBytes chunkSize := s.serveChunkSize + chunkDelay := s.serveChunkDelay s.mu.Unlock() // First chunk MUST carry the metadata header. @@ -735,6 +737,9 @@ func (s *storageRoundTripServer) GetSBOMStream(req *proto.GetSBOMRequest, stream } if chunkSize <= 0 || chunkSize >= len(payload) { + if chunkDelay > 0 { + time.Sleep(chunkDelay) + } return stream.Send(&proto.GetSBOMChunk{BlobChunk: payload}) } for offset := 0; offset < len(payload); offset += chunkSize { @@ -742,6 +747,9 @@ func (s *storageRoundTripServer) GetSBOMStream(req *proto.GetSBOMRequest, stream if end > len(payload) { end = len(payload) } + if chunkDelay > 0 { + time.Sleep(chunkDelay) + } if err := stream.Send(&proto.GetSBOMChunk{BlobChunk: payload[offset:end]}); err != nil { return err } @@ -802,8 +810,9 @@ func (s *storageRoundTripServer) GetContainerProfileStream(req *proto.GetContain } // startBufconnStorageServer starts the round-trip server on an in-memory -// bufconn listener and returns a client connected to it. -func startBufconnStorageServer(t *testing.T) (*storageRoundTripServer, *StorageClient, func()) { +// bufconn listener and returns a client connected to it. Optional client +// options (e.g. WithCallTimeout) are forwarded to NewStorageClient. +func startBufconnStorageServer(t *testing.T, opts ...StorageClientOption) (*storageRoundTripServer, *StorageClient, func()) { t.Helper() const bufsize = 1024 * 1024 lis := bufconn.Listen(bufsize) @@ -819,7 +828,7 @@ func startBufconnStorageServer(t *testing.T) (*storageRoundTripServer, *StorageC ) require.NoError(t, err) - client, err := NewStorageClient("grpc://example.com:50051", "test-account", "test-key", "test-cluster") + client, err := NewStorageClient("grpc://example.com:50051", "test-account", "test-key", "test-cluster", opts...) require.NoError(t, err) client.conn = conn client.protoClient = proto.NewStorageServiceClient(conn) @@ -1099,3 +1108,53 @@ func TestStorageClient_SendContainerProfileStream_NilProfile(t *testing.T) { require.Error(t, err) assert.Contains(t, err.Error(), "nil") } + +// TestStorageClient_GetSBOMStream_NotBoundedByCallTimeout proves that the +// streaming RPC is NOT clipped by WithCallTimeout — a regression matthyx +// flagged. We configure a very short callTimeout (well under any +// production stream latency) and have the server delay between chunks +// by an order of magnitude longer. If the stream were wrapped in +// context.WithTimeout(callTimeout) the call would fail with +// "context deadline exceeded"; success here demonstrates the timeout is +// only applied to unary RPCs. +// +// Test stays under 500ms wall time so it's safe in CI. +func TestStorageClient_GetSBOMStream_NotBoundedByCallTimeout(t *testing.T) { + const ( + shortCallTimeout = 20 * time.Millisecond + serverChunkDelay = 50 * time.Millisecond + chunks = 5 + ) + // Total stream time on the server ≈ chunks * serverChunkDelay = 250ms, + // which is 12× the configured callTimeout. A unary timeout would have + // fired within the first 20ms. + + rtSrv, client, cleanup := startBufconnStorageServer(t, WithCallTimeout(shortCallTimeout)) + defer cleanup() + + original := sampleSBOMSyft() + payload, err := original.Marshal() + require.NoError(t, err) + require.GreaterOrEqual(t, len(payload), chunks, "payload must have enough bytes to split") + + rtSrv.serveExists = true + rtSrv.serveMetadata = &proto.SBOMMetadata{ImageDigest: "abc", SyftVersion: "1.0.0"} + rtSrv.serveBytes = payload + rtSrv.serveChunkSize = len(payload) / chunks + rtSrv.serveChunkDelay = serverChunkDelay + + start := time.Now() + md, reader, err := client.GetSBOMStream(context.Background(), "abc", "1.0.0", false) + require.NoError(t, err, "stream must not be clipped by callTimeout") + require.NotNil(t, reader) + defer reader.Close() + assert.True(t, md.Exists) + + got, err := UnmarshalSBOM(reader) + require.NoError(t, err) + assert.Equal(t, original.Name, got.Name) + + elapsed := time.Since(start) + assert.Greater(t, elapsed, shortCallTimeout, + "sanity: the stream genuinely ran longer than callTimeout (otherwise the test proves nothing)") +}