vendor: update buildkit to v0.32.0-rc1

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi
2026-07-22 15:27:47 -07:00
parent efd9aa1dea
commit 0cf7592d41
464 changed files with 19941 additions and 15772 deletions
File diff suppressed because it is too large Load Diff
+220 -221
View File
@@ -1,330 +1,329 @@
/*
Copyright The containerd Authors.
Copyright The containerd Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
syntax = "proto3";
package containerd.services.content.v1;
import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto";
import "google/protobuf/empty.proto";
option go_package = "github.com/containerd/containerd/api/services/content/v1;content";
// Content provides access to a content addressable storage system.
service Content {
// Info returns information about a committed object.
//
// This call can be used for getting the size of content and checking for
// existence.
rpc Info(InfoRequest) returns (InfoResponse);
// Info returns information about a committed object.
//
// This call can be used for getting the size of content and checking for
// existence.
rpc Info(InfoRequest) returns (InfoResponse);
// Update updates content metadata.
//
// This call can be used to manage the mutable content labels. The
// immutable metadata such as digest, size, and committed at cannot
// be updated.
rpc Update(UpdateRequest) returns (UpdateResponse);
// Update updates content metadata.
//
// This call can be used to manage the mutable content labels. The
// immutable metadata such as digest, size, and committed at cannot
// be updated.
rpc Update(UpdateRequest) returns (UpdateResponse);
// List streams the entire set of content as Info objects and closes the
// stream.
//
// Typically, this will yield a large response, chunked into messages.
// Clients should make provisions to ensure they can handle the entire data
// set.
rpc List(ListContentRequest) returns (stream ListContentResponse);
// List streams the entire set of content as Info objects and closes the
// stream.
//
// Typically, this will yield a large response, chunked into messages.
// Clients should make provisions to ensure they can handle the entire data
// set.
rpc List(ListContentRequest) returns (stream ListContentResponse);
// Delete will delete the referenced object.
rpc Delete(DeleteContentRequest) returns (google.protobuf.Empty);
// Delete will delete the referenced object.
rpc Delete(DeleteContentRequest) returns (google.protobuf.Empty);
// Read allows one to read an object based on the offset into the content.
//
// The requested data may be returned in one or more messages.
rpc Read(ReadContentRequest) returns (stream ReadContentResponse);
// Read allows one to read an object based on the offset into the content.
//
// The requested data may be returned in one or more messages.
rpc Read(ReadContentRequest) returns (stream ReadContentResponse);
// Status returns the status for a single reference.
rpc Status(StatusRequest) returns (StatusResponse);
// Status returns the status for a single reference.
rpc Status(StatusRequest) returns (StatusResponse);
// ListStatuses returns the status of ongoing object ingestions, started via
// Write.
//
// Only those matching the regular expression will be provided in the
// response. If the provided regular expression is empty, all ingestions
// will be provided.
rpc ListStatuses(ListStatusesRequest) returns (ListStatusesResponse);
// ListStatuses returns the status of ongoing object ingestions, started via
// Write.
//
// Only those matching the regular expression will be provided in the
// response. If the provided regular expression is empty, all ingestions
// will be provided.
rpc ListStatuses(ListStatusesRequest) returns (ListStatusesResponse);
// Write begins or resumes writes to a resource identified by a unique ref.
// Only one active stream may exist at a time for each ref.
//
// Once a write stream has started, it may only write to a single ref, thus
// once a stream is started, the ref may be omitted on subsequent writes.
//
// For any write transaction represented by a ref, only a single write may
// be made to a given offset. If overlapping writes occur, it is an error.
// Writes should be sequential and implementations may throw an error if
// this is required.
//
// If expected_digest is set and already part of the content store, the
// write will fail.
//
// When completed, the commit flag should be set to true. If expected size
// or digest is set, the content will be validated against those values.
rpc Write(stream WriteContentRequest) returns (stream WriteContentResponse);
// Write begins or resumes writes to a resource identified by a unique ref.
// Only one active stream may exist at a time for each ref.
//
// Once a write stream has started, it may only write to a single ref, thus
// once a stream is started, the ref may be omitted on subsequent writes.
//
// For any write transaction represented by a ref, only a single write may
// be made to a given offset. If overlapping writes occur, it is an error.
// Writes should be sequential and implementations may throw an error if
// this is required.
//
// If expected_digest is set and already part of the content store, the
// write will fail.
//
// When completed, the commit flag should be set to true. If expected size
// or digest is set, the content will be validated against those values.
rpc Write(stream WriteContentRequest) returns (stream WriteContentResponse);
// Abort cancels the ongoing write named in the request. Any resources
// associated with the write will be collected.
rpc Abort(AbortRequest) returns (google.protobuf.Empty);
// Abort cancels the ongoing write named in the request. Any resources
// associated with the write will be collected.
rpc Abort(AbortRequest) returns (google.protobuf.Empty);
}
message Info {
// Digest is the hash identity of the blob.
string digest = 1;
// Digest is the hash identity of the blob.
string digest = 1;
// Size is the total number of bytes in the blob.
int64 size = 2;
// Size is the total number of bytes in the blob.
int64 size = 2;
// CreatedAt provides the time at which the blob was committed.
google.protobuf.Timestamp created_at = 3;
// CreatedAt provides the time at which the blob was committed.
google.protobuf.Timestamp created_at = 3;
// UpdatedAt provides the time the info was last updated.
google.protobuf.Timestamp updated_at = 4;
// UpdatedAt provides the time the info was last updated.
google.protobuf.Timestamp updated_at = 4;
// Labels are arbitrary data on snapshots.
//
// The combined size of a key/value pair cannot exceed 4096 bytes.
map<string, string> labels = 5;
// Labels are arbitrary data on snapshots.
//
// The combined size of a key/value pair cannot exceed 4096 bytes.
map<string, string> labels = 5;
}
message InfoRequest {
string digest = 1;
string digest = 1;
}
message InfoResponse {
Info info = 1;
Info info = 1;
}
message UpdateRequest {
Info info = 1;
Info info = 1;
// UpdateMask specifies which fields to perform the update on. If empty,
// the operation applies to all fields.
//
// In info, Digest, Size, and CreatedAt are immutable,
// other field may be updated using this mask.
// If no mask is provided, all mutable field are updated.
google.protobuf.FieldMask update_mask = 2;
// UpdateMask specifies which fields to perform the update on. If empty,
// the operation applies to all fields.
//
// In info, Digest, Size, and CreatedAt are immutable,
// other field may be updated using this mask.
// If no mask is provided, all mutable field are updated.
google.protobuf.FieldMask update_mask = 2;
}
message UpdateResponse {
Info info = 1;
Info info = 1;
}
message ListContentRequest {
// Filters contains one or more filters using the syntax defined in the
// containerd filter package.
//
// The returned result will be those that match any of the provided
// filters. Expanded, containers that match the following will be
// returned:
//
// filters[0] or filters[1] or ... or filters[n-1] or filters[n]
//
// If filters is zero-length or nil, all items will be returned.
repeated string filters = 1;
// Filters contains one or more filters using the syntax defined in the
// containerd filter package.
//
// The returned result will be those that match any of the provided
// filters. Expanded, containers that match the following will be
// returned:
//
// filters[0] or filters[1] or ... or filters[n-1] or filters[n]
//
// If filters is zero-length or nil, all items will be returned.
repeated string filters = 1;
}
message ListContentResponse {
repeated Info info = 1;
repeated Info info = 1;
}
message DeleteContentRequest {
// Digest specifies which content to delete.
string digest = 1;
// Digest specifies which content to delete.
string digest = 1;
}
// ReadContentRequest defines the fields that make up a request to read a portion of
// data from a stored object.
message ReadContentRequest {
// Digest is the hash identity to read.
string digest = 1;
// Digest is the hash identity to read.
string digest = 1;
// Offset specifies the number of bytes from the start at which to begin
// the read. If zero or less, the read will be from the start. This uses
// standard zero-indexed semantics.
int64 offset = 2;
// Offset specifies the number of bytes from the start at which to begin
// the read. If zero or less, the read will be from the start. This uses
// standard zero-indexed semantics.
int64 offset = 2;
// size is the total size of the read. If zero, the entire blob will be
// returned by the service.
int64 size = 3;
// size is the total size of the read. If zero, the entire blob will be
// returned by the service.
int64 size = 3;
}
// ReadContentResponse carries byte data for a read request.
message ReadContentResponse {
int64 offset = 1; // offset of the returned data
bytes data = 2; // actual data
int64 offset = 1; // offset of the returned data
bytes data = 2; // actual data
}
message Status {
google.protobuf.Timestamp started_at = 1;
google.protobuf.Timestamp updated_at = 2;
string ref = 3;
int64 offset = 4;
int64 total = 5;
string expected = 6;
google.protobuf.Timestamp started_at = 1;
google.protobuf.Timestamp updated_at = 2;
string ref = 3;
int64 offset = 4;
int64 total = 5;
string expected = 6;
}
message StatusRequest {
string ref = 1;
string ref = 1;
}
message StatusResponse {
Status status = 1;
Status status = 1;
}
message ListStatusesRequest {
repeated string filters = 1;
repeated string filters = 1;
}
message ListStatusesResponse {
repeated Status statuses = 1;
repeated Status statuses = 1;
}
// WriteAction defines the behavior of a WriteRequest.
enum WriteAction {
// WriteActionStat instructs the writer to return the current status while
// holding the lock on the write.
STAT = 0;
// WriteActionStat instructs the writer to return the current status while
// holding the lock on the write.
STAT = 0;
// WriteActionWrite sets the action for the write request to write data.
//
// Any data included will be written at the provided offset. The
// transaction will be left open for further writes.
//
// This is the default.
WRITE = 1;
// WriteActionWrite sets the action for the write request to write data.
//
// Any data included will be written at the provided offset. The
// transaction will be left open for further writes.
//
// This is the default.
WRITE = 1;
// WriteActionCommit will write any outstanding data in the message and
// commit the write, storing it under the digest.
//
// This can be used in a single message to send the data, verify it and
// commit it.
//
// This action will always terminate the write.
COMMIT = 2;
// WriteActionCommit will write any outstanding data in the message and
// commit the write, storing it under the digest.
//
// This can be used in a single message to send the data, verify it and
// commit it.
//
// This action will always terminate the write.
COMMIT = 2;
}
// WriteContentRequest writes data to the request ref at offset.
message WriteContentRequest {
// Action sets the behavior of the write.
//
// When this is a write and the ref is not yet allocated, the ref will be
// allocated and the data will be written at offset.
//
// If the action is write and the ref is allocated, it will accept data to
// an offset that has not yet been written.
//
// If the action is write and there is no data, the current write status
// will be returned. This works differently from status because the stream
// holds a lock.
WriteAction action = 1;
// Action sets the behavior of the write.
//
// When this is a write and the ref is not yet allocated, the ref will be
// allocated and the data will be written at offset.
//
// If the action is write and the ref is allocated, it will accept data to
// an offset that has not yet been written.
//
// If the action is write and there is no data, the current write status
// will be returned. This works differently from status because the stream
// holds a lock.
WriteAction action = 1;
// Ref identifies the pre-commit object to write to.
string ref = 2;
// Ref identifies the pre-commit object to write to.
string ref = 2;
// Total can be set to have the service validate the total size of the
// committed content.
//
// The latest value before or with the commit action message will be use to
// validate the content. If the offset overflows total, the service may
// report an error. It is only required on one message for the write.
//
// If the value is zero or less, no validation of the final content will be
// performed.
int64 total = 3;
// Total can be set to have the service validate the total size of the
// committed content.
//
// The latest value before or with the commit action message will be use to
// validate the content. If the offset overflows total, the service may
// report an error. It is only required on one message for the write.
//
// If the value is zero or less, no validation of the final content will be
// performed.
int64 total = 3;
// Expected can be set to have the service validate the final content against
// the provided digest.
//
// If the digest is already present in the object store, an AlreadyExists
// error will be returned.
//
// Only the latest version will be used to check the content against the
// digest. It is only required to include it on a single message, before or
// with the commit action message.
string expected = 4;
// Expected can be set to have the service validate the final content against
// the provided digest.
//
// If the digest is already present in the object store, an AlreadyExists
// error will be returned.
//
// Only the latest version will be used to check the content against the
// digest. It is only required to include it on a single message, before or
// with the commit action message.
string expected = 4;
// Offset specifies the number of bytes from the start at which to begin
// the write. For most implementations, this means from the start of the
// file. This uses standard, zero-indexed semantics.
//
// If the action is write, the remote may remove all previously written
// data after the offset. Implementations may support arbitrary offsets but
// MUST support reseting this value to zero with a write. If an
// implementation does not support a write at a particular offset, an
// OutOfRange error must be returned.
int64 offset = 5;
// Offset specifies the number of bytes from the start at which to begin
// the write. For most implementations, this means from the start of the
// file. This uses standard, zero-indexed semantics.
//
// If the action is write, the remote may remove all previously written
// data after the offset. Implementations may support arbitrary offsets but
// MUST support reseting this value to zero with a write. If an
// implementation does not support a write at a particular offset, an
// OutOfRange error must be returned.
int64 offset = 5;
// Data is the actual bytes to be written.
//
// If this is empty and the message is not a commit, a response will be
// returned with the current write state.
bytes data = 6;
// Data is the actual bytes to be written.
//
// If this is empty and the message is not a commit, a response will be
// returned with the current write state.
bytes data = 6;
// Labels are arbitrary data on snapshots.
//
// The combined size of a key/value pair cannot exceed 4096 bytes.
map<string, string> labels = 7;
// Labels are arbitrary data on snapshots.
//
// The combined size of a key/value pair cannot exceed 4096 bytes.
map<string, string> labels = 7;
}
// WriteContentResponse is returned on the culmination of a write call.
message WriteContentResponse {
// Action contains the action for the final message of the stream. A writer
// should confirm that they match the intended result.
WriteAction action = 1;
// Action contains the action for the final message of the stream. A writer
// should confirm that they match the intended result.
WriteAction action = 1;
// StartedAt provides the time at which the write began.
//
// This must be set for stat and commit write actions. All other write
// actions may omit this.
google.protobuf.Timestamp started_at = 2;
// StartedAt provides the time at which the write began.
//
// This must be set for stat and commit write actions. All other write
// actions may omit this.
google.protobuf.Timestamp started_at = 2;
// UpdatedAt provides the last time of a successful write.
//
// This must be set for stat and commit write actions. All other write
// actions may omit this.
google.protobuf.Timestamp updated_at = 3;
// UpdatedAt provides the last time of a successful write.
//
// This must be set for stat and commit write actions. All other write
// actions may omit this.
google.protobuf.Timestamp updated_at = 3;
// Offset is the current committed size for the write.
int64 offset = 4;
// Offset is the current committed size for the write.
int64 offset = 4;
// Total provides the current, expected total size of the write.
//
// We include this to provide consistency with the Status structure on the
// client writer.
//
// This is only valid on the Stat and Commit response.
int64 total = 5;
// Total provides the current, expected total size of the write.
//
// We include this to provide consistency with the Status structure on the
// client writer.
//
// This is only valid on the Stat and Commit response.
int64 total = 5;
// Digest, if present, includes the digest up to the currently committed
// bytes. If action is commit, this field will be set. It is implementation
// defined if this is set for other actions.
string digest = 6;
// Digest, if present, includes the digest up to the currently committed
// bytes. If action is commit, this field will be set. It is implementation
// defined if this is set for other actions.
string digest = 6;
}
message AbortRequest {
string ref = 1;
string ref = 1;
}
@@ -3,8 +3,8 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.2.0
// - protoc v3.20.1
// source: github.com/containerd/containerd/api/services/content/v1/content.proto
// - protoc (unknown)
// source: services/content/v1/content.proto
package content
@@ -567,5 +567,5 @@ var Content_ServiceDesc = grpc.ServiceDesc{
ClientStreams: true,
},
},
Metadata: "github.com/containerd/containerd/api/services/content/v1/content.proto",
Metadata: "services/content/v1/content.proto",
}
@@ -1,5 +1,5 @@
// Code generated by protoc-gen-go-ttrpc. DO NOT EDIT.
// source: github.com/containerd/containerd/api/services/content/v1/content.proto
// source: services/content/v1/content.proto
package content
import (
+21 -3
View File
@@ -35,7 +35,7 @@ import (
var ErrReset = errors.New("writer has been reset")
var bufPool = sync.Pool{
New: func() interface{} {
New: func() any {
buffer := make([]byte, 1<<20)
return &buffer
},
@@ -65,9 +65,23 @@ type nopCloserSectionReader struct {
func (*nopCloserSectionReader) Close() error { return nil }
func useDescriptorData(desc ocispec.Descriptor) (bool, error) {
if int64(len(desc.Data)) != desc.Size {
return false, nil
}
if err := desc.Digest.Validate(); err != nil {
return false, fmt.Errorf("invalid descriptor digest: %w", err)
}
return desc.Digest.Algorithm().FromBytes(desc.Data) == desc.Digest, nil
}
// BlobReadSeeker returns a read seeker for the blob from the provider.
func BlobReadSeeker(ctx context.Context, provider Provider, desc ocispec.Descriptor) (io.ReadSeekCloser, error) {
if int64(len(desc.Data)) == desc.Size && digest.FromBytes(desc.Data) == desc.Digest {
useData, err := useDescriptorData(desc)
if err != nil {
return nil, err
}
if useData {
return &nopCloserBytesReader{bytes.NewReader(desc.Data)}, nil
}
@@ -82,7 +96,11 @@ func BlobReadSeeker(ctx context.Context, provider Provider, desc ocispec.Descrip
//
// Avoid using this for large blobs, such as layers.
func ReadBlob(ctx context.Context, provider Provider, desc ocispec.Descriptor) ([]byte, error) {
if int64(len(desc.Data)) == desc.Size && digest.FromBytes(desc.Data) == desc.Digest {
useData, err := useDescriptorData(desc)
if err != nil {
return nil, err
}
if useData {
return desc.Data, nil
}
@@ -22,6 +22,7 @@ import (
"encoding/json"
"fmt"
"io"
"maps"
"path"
"slices"
"sort"
@@ -195,9 +196,7 @@ func WithSkipMissing(store content.InfoReaderProvider) ExportOpt {
func addNameAnnotation(name string, base map[string]string) map[string]string {
annotations := map[string]string{}
for k, v := range base {
annotations[k] = v
}
maps.Copy(annotations, base)
annotations[images.AnnotationImageName] = name
annotations[ocispec.AnnotationRefName] = ociReferenceName(name)
@@ -241,7 +241,7 @@ const (
jsonLimit = 20 * mib
)
func onUntarJSON(r io.Reader, j interface{}) error {
func onUntarJSON(r io.Reader, j any) error {
return json.NewDecoder(io.LimitReader(r, jsonLimit)).Decode(j)
}
@@ -376,7 +376,7 @@ func compressBlob(ctx context.Context, cs content.Store, r io.Reader, ref string
return desc, nil
}
func writeManifest(ctx context.Context, cs content.Ingester, manifest interface{}, mediaType string) (ocispec.Descriptor, error) {
func writeManifest(ctx context.Context, cs content.Ingester, manifest any, mediaType string) (ocispec.Descriptor, error) {
manifestBytes, err := json.Marshal(manifest)
if err != nil {
return ocispec.Descriptor{}, err
@@ -91,12 +91,14 @@ func familiarizeReference(ref string) (string, error) {
}
func ociReferenceName(name string) string {
// OCI defines the reference name as only a tag excluding the
// OCI suggests the reference name as only a tag excluding the
// repository. The containerd annotation contains the full image name
// since the tag is insufficient for correctly naming and referring to an
// image
// image. In the case of a by-digest image referencing only a SHA hash,
// the full image name with hash is preferred to match the required
// grammar of the OCI spec.
var ociRef string
if spec, err := reference.Parse(name); err == nil {
if spec, err := reference.Parse(name); err == nil && spec.Object != "" && spec.Object[0] != '@' {
ociRef = spec.Object
} else {
ociRef = name
+7 -1
View File
@@ -59,6 +59,9 @@ const (
MediaTypeImageLayerEncrypted = ocispec.MediaTypeImageLayer + "+encrypted"
MediaTypeImageLayerGzipEncrypted = ocispec.MediaTypeImageLayerGzip + "+encrypted"
// EROFS media type
MediaTypeErofsLayer = "application/vnd.erofs.layer.v1"
// In-toto attestation
MediaTypeInToto = "application/vnd.in-toto+json"
)
@@ -139,11 +142,14 @@ func IsLayerType(mt string) bool {
return true
}
// Parse Docker media types, strip off any + suffixes first
switch base, _ := parseMediaTypes(mt); base {
// Parse Docker media types, strip off any + suffixes first
case MediaTypeDockerSchema2Layer, MediaTypeDockerSchema2LayerGzip,
MediaTypeDockerSchema2LayerForeign, MediaTypeDockerSchema2LayerForeignGzip, MediaTypeDockerSchema2LayerZstd:
return true
// Allow EROFS native layers for efficient container image distribution.
case MediaTypeErofsLayer:
return true
}
return false
}
+2 -3
View File
@@ -18,6 +18,7 @@ package leases
import (
"context"
"maps"
"time"
)
@@ -84,9 +85,7 @@ func WithLabels(labels map[string]string) Opt {
if l.Labels == nil {
l.Labels = map[string]string{}
}
for k, v := range labels {
l.Labels[k] = v
}
maps.Copy(l.Labels, labels)
return nil
}
}
@@ -79,7 +79,7 @@ func init() {
// token = 1*<any CHAR except CTLs or separators>
// qdtext = <any TEXT except <">>
for c := 0; c < 256; c++ {
for c := range 256 {
var t octetType
isCtl := c <= 31 || c == 127
isChar := 0 <= c && c <= 127
+12 -11
View File
@@ -100,7 +100,7 @@ func (ec ErrorCode) WithMessage(message string) Error {
// WithDetail creates a new Error struct based on the passed-in info and
// set the Detail property appropriately
func (ec ErrorCode) WithDetail(detail interface{}) Error {
func (ec ErrorCode) WithDetail(detail any) Error {
return Error{
Code: ec,
Message: ec.Message(),
@@ -108,7 +108,7 @@ func (ec ErrorCode) WithDetail(detail interface{}) Error {
}
// WithArgs creates a new Error struct and sets the Args slice
func (ec ErrorCode) WithArgs(args ...interface{}) Error {
func (ec ErrorCode) WithArgs(args ...any) Error {
return Error{
Code: ec,
Message: ec.Message(),
@@ -117,9 +117,9 @@ func (ec ErrorCode) WithArgs(args ...interface{}) Error {
// Error provides a wrapper around ErrorCode with extra Details provided.
type Error struct {
Code ErrorCode `json:"code"`
Message string `json:"message"`
Detail interface{} `json:"detail,omitempty"`
Code ErrorCode `json:"code"`
Message string `json:"message"`
Detail any `json:"detail,omitempty"`
// TODO(duglin): See if we need an "args" property so we can do the
// variable substitution right before showing the message to the user
@@ -139,7 +139,7 @@ func (e Error) Error() string {
// WithDetail will return a new Error, based on the current one, but with
// some Detail info added
func (e Error) WithDetail(detail interface{}) Error {
func (e Error) WithDetail(detail any) Error {
return Error{
Code: e.Code,
Message: e.Message,
@@ -147,9 +147,9 @@ func (e Error) WithDetail(detail interface{}) Error {
}
}
// WithArgs uses the passed-in list of interface{} as the substitution
// WithArgs uses the passed-in list of args as the substitution
// variables in the Error's Message string, but returns a new Error
func (e Error) WithArgs(args ...interface{}) Error {
func (e Error) WithArgs(args ...any) Error {
return Error{
Code: e.Code,
Message: fmt.Sprintf(e.Code.Message(), args...),
@@ -204,11 +204,12 @@ func (errs Errors) Error() string {
case 1:
return errs[0].Error()
default:
msg := "errors:\n"
var msg strings.Builder
msg.WriteString("errors:\n")
for _, err := range errs {
msg += err.Error() + "\n"
msg.WriteString(err.Error() + "\n")
}
return msg
return msg.String()
}
}
+24 -20
View File
@@ -34,6 +34,7 @@ import (
"github.com/klauspost/compress/zstd"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"golang.org/x/sync/errgroup"
"github.com/containerd/containerd/v2/core/images"
"github.com/containerd/containerd/v2/core/remotes"
@@ -60,6 +61,7 @@ func (p *bufferPool) Get() *bytes.Buffer {
}
func (p *bufferPool) Put(buffer *bytes.Buffer) {
buffer.Reset()
p.pool.Put(buffer)
}
@@ -347,7 +349,7 @@ func (r dockerFetcher) createGetReq(ctx context.Context, host RegistryHost, last
headResp.Body.Close()
}
if headResp.StatusCode > 299 {
return nil, 0, fmt.Errorf("unexpected HEAD status code %v: %s", headReq.String(), headResp.Status)
return nil, 0, fmt.Errorf("unexpected HEAD status code %v: %s", headReq.sanitizedURL(), headResp.Status)
}
getReq := r.request(host, http.MethodGet, ps...)
@@ -510,9 +512,11 @@ func (r dockerFetcher) open(ctx context.Context, req *request, mediatype string,
if numChunks < parallelism {
parallelism = numChunks
}
// Prepare channels, buffer pool, and readers/writers for parallel fetching.
queue := make(chan int64, parallelism)
ctx, cancelCtx := context.WithCancel(ctx)
done := ctx.Done()
ctx, cancel := context.WithCancel(ctx)
eg, ctx := errgroup.WithContext(ctx)
readers, writers := make([]io.Reader, numChunks), make([]*pipeWriter, numChunks)
bufPool := newbufferPool(chunkSize)
for i := range numChunks {
@@ -520,21 +524,23 @@ func (r dockerFetcher) open(ctx context.Context, req *request, mediatype string,
}
// keep reference of the initial body value to ensure it is closed
ibody := body
go func() {
eg.Go(func() error {
defer close(queue)
for i := range numChunks {
select {
case queue <- i:
case <-done:
case <-ctx.Done():
if i == 0 {
ibody.Close()
}
return // avoid leaking a goroutine if we exit early.
return ctx.Err()
}
}
close(queue)
}()
return nil
})
for range parallelism {
go func() {
eg.Go(func() error {
for i := range queue { // first in first out
copy := func() error {
var body io.ReadCloser
@@ -542,6 +548,7 @@ func (r dockerFetcher) open(ctx context.Context, req *request, mediatype string,
body = ibody
} else {
if err := r.Acquire(ctx, 1); err != nil {
_ = writers[i].CloseWithError(err)
return err
}
defer r.Release(1)
@@ -550,12 +557,6 @@ func (r dockerFetcher) open(ctx context.Context, req *request, mediatype string,
nresp, err := reqClone.doWithRetries(ctx, lastHost, withErrorCheck)
if err != nil {
_ = writers[i].CloseWithError(err)
select {
case <-done:
return ctx.Err()
default:
cancelCtx()
}
return err
}
body = nresp.Body
@@ -564,20 +565,23 @@ func (r dockerFetcher) open(ctx context.Context, req *request, mediatype string,
_ = body.Close()
_ = writers[i].CloseWithError(err)
if err != nil && err != io.EOF {
cancelCtx()
return err
}
return nil
}
if copy() != nil {
return
if err := copy(); err != nil {
return err
}
}
}()
return nil
})
}
body = &fnOnClose{
BeforeClose: func() {
cancelCtx()
cancel()
if err := eg.Wait(); err != nil {
log.G(ctx).WithError(err).Warn("parallel fetch failed")
}
},
ReadCloser: io.NopCloser(io.MultiReader(readers...)),
}
+1 -1
View File
@@ -123,7 +123,7 @@ func selectRepositoryMountCandidate(refspec reference.Spec, sources map[string]s
n, match := 0, ""
components := strings.Split(target, "/")
for _, repo := range strings.Split(repoLabel, ",") {
for repo := range strings.SplitSeq(repoLabel, ",") {
// the target repo is not a candidate
if repo == target {
continue
@@ -105,7 +105,7 @@ func (hrs *httpReadSeeker) Close() error {
func (hrs *httpReadSeeker) Seek(offset int64, whence int) (int64, error) {
if hrs.closed {
return 0, fmt.Errorf("Fetcher.Seek: closed: %w", errdefs.ErrUnavailable)
return 0, fmt.Errorf("httpReadSeeker.Seek: closed: %w", errdefs.ErrUnavailable)
}
abs := hrs.offset
@@ -116,21 +116,21 @@ func (hrs *httpReadSeeker) Seek(offset int64, whence int) (int64, error) {
abs += offset
case io.SeekEnd:
if hrs.size == -1 {
return 0, fmt.Errorf("Fetcher.Seek: unknown size, cannot seek from end: %w", errdefs.ErrUnavailable)
return 0, fmt.Errorf("httpReadSeeker.Seek: unknown size, cannot seek from end: %w", errdefs.ErrUnavailable)
}
abs = hrs.size + offset
default:
return 0, fmt.Errorf("Fetcher.Seek: invalid whence: %w", errdefs.ErrInvalidArgument)
return 0, fmt.Errorf("httpReadSeeker.Seek: invalid whence: %w", errdefs.ErrInvalidArgument)
}
if abs < 0 {
return 0, fmt.Errorf("Fetcher.Seek: negative offset: %w", errdefs.ErrInvalidArgument)
return 0, fmt.Errorf("httpReadSeeker.Seek: negative offset: %w", errdefs.ErrInvalidArgument)
}
if abs != hrs.offset {
if hrs.rc != nil {
if err := hrs.rc.Close(); err != nil {
log.L.WithError(err).Error("Fetcher.Seek: failed to close ReadCloser")
log.L.WithError(err).Error("httpReadSeeker.Seek: failed to close ReadCloser")
}
hrs.rc = nil
+66 -1
View File
@@ -60,6 +60,12 @@ func (p dockerPusher) Writer(ctx context.Context, opts ...content.WriterOpt) (co
if wOpts.Ref == "" {
return nil, fmt.Errorf("ref must not be empty: %w", errdefs.ErrInvalidArgument)
}
if wOpts.Desc.Digest == "" {
return nil, fmt.Errorf("descriptor digest must not be empty: %w", errdefs.ErrInvalidArgument)
}
if wOpts.Desc.MediaType == "" {
return nil, fmt.Errorf("descriptor media type must not be empty: %w", errdefs.ErrInvalidArgument)
}
return p.push(ctx, wOpts.Desc, wOpts.Ref, true)
}
@@ -111,9 +117,12 @@ func (p dockerPusher) push(ctx context.Context, desc ocispec.Descriptor, ref str
}
req := p.request(host, http.MethodHead, existCheck...)
if err := req.addNamespace(p.refspec.Hostname()); err != nil {
return nil, err
}
req.header.Set("Accept", strings.Join([]string{desc.MediaType, `*/*`}, ", "))
log.G(ctx).WithField("url", req.String()).Debugf("checking and pushing to")
log.G(ctx).WithField("url", req.sanitizedURL()).Debugf("checking and pushing to")
resp, err := req.doWithRetries(ctx, true)
if err != nil {
@@ -149,6 +158,18 @@ func (p dockerPusher) push(ctx context.Context, desc ocispec.Descriptor, ref str
}
} else if resp.StatusCode != http.StatusNotFound {
err := unexpectedResponseErr(resp)
// A HEAD 403 carries no body, so issue a follow-up GET to the
// same URL to surface the registry's error details for diagnostics.
if resp.StatusCode == http.StatusForbidden && req.method == http.MethodHead {
err = withGETErrorBody(ctx, err, resp, func() (*http.Response, error) {
getReq := p.request(host, http.MethodGet, existCheck...)
getReq.header.Set("Accept", strings.Join([]string{desc.MediaType, `*/*`}, ", "))
if addErr := getReq.addNamespace(p.refspec.Hostname()); addErr != nil {
return nil, addErr
}
return getReq.doWithRetries(ctx, false)
})
}
log.G(ctx).WithError(err).Debug("unexpected response")
resp.Body.Close()
return nil, err
@@ -159,10 +180,16 @@ func (p dockerPusher) push(ctx context.Context, desc ocispec.Descriptor, ref str
if isManifest {
putPath := getManifestPath(p.object, desc.Digest)
req = p.request(host, http.MethodPut, putPath...)
if err := req.addNamespace(p.refspec.Hostname()); err != nil {
return nil, err
}
req.header.Add("Content-Type", desc.MediaType)
} else {
// Start upload request
req = p.request(host, http.MethodPost, "blobs", "uploads/")
if err := req.addNamespace(p.refspec.Hostname()); err != nil {
return nil, err
}
mountedFrom := ""
var resp *http.Response
@@ -267,6 +294,9 @@ func (p dockerPusher) push(ctx context.Context, desc ocispec.Descriptor, ref str
req = p.request(lhost, http.MethodPut)
req.header.Set("Content-Type", "application/octet-stream")
req.path = lurl.Path + "?" + q.Encode()
if err := req.addNamespace(p.refspec.Hostname()); err != nil {
return nil, err
}
}
p.tracker.SetStatus(ref, Status{
Status: content.Status{
@@ -556,6 +586,41 @@ func (pw *pushWriter) Truncate(size int64) error {
return errors.New("cannot truncate remote upload")
}
// withGETErrorBody enriches originalErr, produced from a bodyless HEAD
// response, with the error body from a follow-up GET to the same URL. HEAD
// responses carry no body, so a 403 only surfaces its status code; a GET
// returns the registry's error details (e.g. "key vault access denied", IP
// restrictions) that explain the failure.
//
// The GET body is only used when the GET also returns 403, so the enriched
// error's status and body stay consistent. In that case the original HEAD
// request's method and status are preserved and only the body is borrowed from
// the GET; any other outcome (GET failed, or returned a different status)
// leaves originalErr untouched.
func withGETErrorBody(ctx context.Context, originalErr error, headResp *http.Response, doGET func() (*http.Response, error)) error {
getResp, err := doGET()
if err != nil {
log.G(ctx).WithError(err).Debug("failed to retrieve error body with GET fallback")
return originalErr
}
defer getResp.Body.Close()
if getResp.StatusCode != http.StatusForbidden {
log.G(ctx).WithFields(log.Fields{
"head_status": headResp.Status,
"get_status": getResp.Status,
}).Debug("ignoring GET fallback response with different status")
return originalErr
}
// Preserve the original HEAD request's method and status and borrow only
// the body from the GET, so the error still reflects the request the caller
// actually made.
enriched := *headResp
enriched.Body = getResp.Body
return unexpectedResponseErr(&enriched)
}
func requestWithMountFrom(req *request, mount, from string) *request {
creq := *req
@@ -264,5 +264,6 @@ func DefaultHTTPTransport(defaultTLSConfig *tls.Config) *http.Transport {
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: defaultTLSConfig,
ExpectContinueTimeout: 5 * time.Second,
ResponseHeaderTimeout: 30 * time.Second,
}
}
+135 -18
View File
@@ -29,6 +29,7 @@ import (
"path"
"strings"
"sync"
"time"
"github.com/containerd/errdefs"
"github.com/containerd/log"
@@ -292,9 +293,15 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
}
for _, u := range paths {
// falling back to /blobs endpoint should happen in extreme cases - those to
// support legacy registries. we want to limit the fallback to when /manifests endpoint
// returned 404. Falling back on transient errors could do more harm, like polluting
// the local content store with incorrectly typed descriptors as /blobs endpoint tends
// always return with application/octet-stream.
if firstErrPriority > 2 {
break
}
for i, host := range hosts {
ctx := log.WithLogger(ctx, log.G(ctx).WithField("host", host.Host))
req := base.request(host, http.MethodHead, u...)
if err := req.addNamespace(base.refspec.Hostname()); err != nil {
return "", ocispec.Descriptor{}, err
@@ -304,6 +311,11 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
req.header[key] = append(req.header[key], value...)
}
ctx := log.WithLogger(ctx, log.G(ctx).WithFields(log.Fields{
"host": req.host.Host,
"method": req.method,
"url": req.sanitizedURL(),
}))
log.G(ctx).Debug("resolving")
resp, err := req.doWithRetries(ctx, i == len(hosts)-1)
if err != nil {
@@ -329,8 +341,25 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
continue
}
if resp.StatusCode > 399 {
err := unexpectedResponseErr(resp)
// A HEAD 403 carries no body, so issue a follow-up GET to
// the same URL to surface the registry's error details
// (e.g. "key vault access denied", IP restrictions) for
// diagnostics.
if resp.StatusCode == http.StatusForbidden && req.method == http.MethodHead {
err = withGETErrorBody(ctx, err, resp, func() (*http.Response, error) {
getReq := base.request(host, http.MethodGet, u...)
if addErr := getReq.addNamespace(base.refspec.Hostname()); addErr != nil {
return nil, addErr
}
for key, value := range r.resolveHeader {
getReq.header[key] = append(getReq.header[key], value...)
}
return getReq.doWithRetries(ctx, false)
})
}
if firstErrPriority < 3 {
firstErr = unexpectedResponseErr(resp)
firstErr = err
firstErrPriority = 3
}
log.G(ctx).Infof("%s after status: %s", nextHostOrFail(i), resp.Status)
@@ -374,6 +403,12 @@ func (r *dockerResolver) Resolve(ctx context.Context, ref string) (string, ocisp
return "", ocispec.Descriptor{}, err
}
// Check for error status code
if resp.StatusCode >= http.StatusBadRequest {
defer resp.Body.Close()
return "", ocispec.Descriptor{}, unexpectedResponseErr(resp)
}
bodyReader := countingReader{reader: resp.Body}
contentType = getManifestMediaType(resp)
@@ -571,11 +606,13 @@ func (r *request) addQuery(key, value string) (err error) {
return
}
const namespaceQueryArg = "ns"
func (r *request) addNamespace(ns string) error {
if !r.host.isProxy(ns) {
return nil
}
return r.addQuery("ns", ns)
return r.addQuery(namespaceQueryArg, ns)
}
type request struct {
@@ -594,8 +631,7 @@ func (r *request) clone() *request {
}
func (r *request) do(ctx context.Context) (*http.Response, error) {
u := r.host.Scheme + "://" + r.host.Host + r.path
req, err := http.NewRequestWithContext(ctx, r.method, u, nil)
req, err := http.NewRequestWithContext(ctx, r.method, r.String(), nil)
if err != nil {
return nil, err
}
@@ -616,7 +652,7 @@ func (r *request) do(ctx context.Context) (*http.Response, error) {
}
}
ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", u))
ctx = log.WithLogger(ctx, log.G(ctx).WithField("url", r.sanitizedURL()))
log.G(ctx).WithFields(requestFields(req)).Debug("do request")
if err := r.authorize(ctx, req); err != nil {
return nil, fmt.Errorf("failed to authorize: %w", err)
@@ -653,7 +689,7 @@ type doChecks func(r *request, resp *http.Response) error
func withErrorCheck(r *request, resp *http.Response) error {
if resp.StatusCode > 299 {
if resp.StatusCode == http.StatusNotFound {
return fmt.Errorf("content at %v not found: %w", r.String(), errdefs.ErrNotFound)
return fmt.Errorf("content at %v not found: %w", r.sanitizedURL(), errdefs.ErrNotFound)
}
return unexpectedResponseErr(resp)
@@ -694,8 +730,11 @@ func withOffsetCheck(offset, parallelism int64) doChecks {
}
}
const maxAttempts = 5
func (r *request) doWithRetries(ctx context.Context, lastHost bool, checks ...doChecks) (resp *http.Response, err error) {
resp, err = r.doWithRetriesInner(ctx, nil, lastHost)
attempts := maxAttempts
resp, err = r.doWithRetriesInner(ctx, nil, &attempts, lastHost)
if err != nil {
return nil, err
}
@@ -713,8 +752,8 @@ func (r *request) doWithRetries(ctx context.Context, lastHost bool, checks ...do
return resp, nil
}
func (r *request) doWithRetriesInner(ctx context.Context, responses []*http.Response, lastHost bool) (*http.Response, error) {
resp, err := r.do(ctx)
func (r *request) doWithRetriesInner(ctx context.Context, responses []*http.Response, attempts *int, lastHost bool) (*http.Response, error) {
resp, err := r.doWithTransportRetries(ctx, attempts, lastHost)
if err != nil {
return nil, err
}
@@ -725,17 +764,61 @@ func (r *request) doWithRetriesInner(ctx context.Context, responses []*http.Resp
resp.Body.Close()
return nil, err
}
if retry {
if retry && *attempts > 0 {
resp.Body.Close()
return r.doWithRetriesInner(ctx, responses, lastHost)
return r.doWithRetriesInner(ctx, responses, attempts, lastHost)
}
return resp, err
}
func (r *request) retryRequest(ctx context.Context, responses []*http.Response, lastHost bool) (bool, error) {
if len(responses) > 5 {
return false, nil
// doWithTransportRetries calls r.do, retrying on transient transport errors
// (e.g. response header timeouts). Retries are only attempted on the last host
// to match the response-status retry policy for 5xx errors and preserve mirror
// fallback semantics. Context cancellation stops retries immediately.
func (r *request) doWithTransportRetries(ctx context.Context, attempts *int, lastHost bool) (*http.Response, error) {
for *attempts > 0 {
resp, err := r.do(ctx)
*attempts--
if err == nil {
return resp, nil
}
if !lastHost || !isTransientTransportErr(err) {
return nil, err
}
if ctx.Err() != nil {
return nil, ctx.Err()
}
if *attempts == 0 {
return nil, err
}
log.G(ctx).WithError(err).WithField("attempt", maxAttempts-*attempts).Debug("transient transport error, retrying")
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(50 * time.Millisecond):
}
}
return nil, nil
}
// isTransientTransportErr reports whether err is a transport-level error worth
// retrying. context.Canceled and context.DeadlineExceeded are not considered
// transient.
func isTransientTransportErr(err error) bool {
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return false
}
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
return true
}
if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {
return true
}
return false
}
func (r *request) retryRequest(ctx context.Context, responses []*http.Response, lastHost bool) (bool, error) {
last := responses[len(responses)-1]
switch last.StatusCode {
case http.StatusUnauthorized:
@@ -776,6 +859,40 @@ func (r *request) String() string {
return r.host.Scheme + "://" + r.host.Host + r.path
}
// sanitizedURL returns the request URL with query parameters and auth (if any)
// sanitized. It is intended for errors and logging, and similar to [internal/cri/util.sanitizeURL].
//
// [internal/cri/util.sanitizeURL]: https://github.com/containerd/containerd/blob/v2.2.1/internal/cri/util/sanitize.go#L53-L75
func (r *request) sanitizedURL() string {
rawURL := r.String()
parsed, err := url.Parse(rawURL)
if err != nil {
// URL parsing failed; return original (malformed URLs shouldn't leak tokens)
return rawURL
}
if parsed.RawQuery == "" {
// Fast path: no query arguments to sanitize.
return parsed.Redacted()
}
query := parsed.Query()
for k := range query {
if k == namespaceQueryArg {
// preserve namespace query arguments
continue
}
for i := range query[k] {
if query[k][i] != "" {
query[k][i] = "REDACTED"
}
}
}
parsed.RawQuery = query.Encode()
return parsed.Redacted()
}
func (r *request) setMediaType(mediatype string) {
if mediatype == "" {
r.header.Set("Accept", "*/*")
@@ -789,7 +906,7 @@ func (r *request) setOffset(offset int64) {
}
func requestFields(req *http.Request) log.Fields {
fields := map[string]interface{}{
fields := map[string]any{
"request.method": req.Method,
}
for k, vals := range req.Header {
@@ -810,7 +927,7 @@ func requestFields(req *http.Request) log.Fields {
}
func responseFields(resp *http.Response) log.Fields {
fields := map[string]interface{}{
fields := map[string]any{
"response.status": resp.Status,
}
for k, vals := range resp.Header {
+26 -4
View File
@@ -55,9 +55,27 @@ func WithMediaTypeKeyPrefix(ctx context.Context, mediaType, prefix string) conte
return context.WithValue(ctx, refKeyPrefix{}, values)
}
// MakeRefKey returns a unique reference for the descriptor. This reference can be
// used to lookup ongoing processes related to the descriptor. This function
// may look to the context to namespace the reference appropriately.
// MakeRefKey returns a stable ingest reference for desc.
//
// The returned key is used as a content-store reference to correlate ongoing
// fetch and push operations for the same descriptor. The key is derived from
// the descriptor digest and, when present, the
// [ocispec.AnnotationRefName] annotation.
//
// By default, the key is prefixed according to the descriptor media type:
//
// - "manifest-" for manifest media types recognized by [images.IsManifestType]
// - "index-" for index media types recognized by [images.IsIndexType]
// - "layer-" for layer media types recognized by [images.IsLayerType]
// - "config-" for config media types recognized by [images.IsKnownConfig]
// - "attestation-" for attestation media types recognized by [images.IsAttestationType]
//
// Additional exact media type mappings may be provided through
// [WithMediaTypeKeyPrefix]. A context-provided mapping takes precedence over the
// built-in classification.
//
// If the media type is not recognized and no context override exists,
// MakeRefKey falls back to the "unknown-" prefix.
func MakeRefKey(ctx context.Context, desc ocispec.Descriptor) string {
key := desc.Digest.String()
if desc.Annotations != nil {
@@ -85,7 +103,11 @@ func MakeRefKey(ctx context.Context, desc ocispec.Descriptor) string {
case images.IsAttestationType(desc.MediaType):
return "attestation-" + key
default:
log.G(ctx).Warnf("reference for unknown type: %s", desc.MediaType)
log.G(ctx).WithFields(log.Fields{
"digest": desc.Digest,
"mediatype": desc.MediaType,
"artifactType": desc.ArtifactType,
}).Debug("using generic reference key prefix for unclassified descriptor")
return "unknown-" + key
}
}
+1 -1
View File
@@ -29,7 +29,7 @@ import (
)
type Transferrer interface {
Transfer(ctx context.Context, source interface{}, destination interface{}, opts ...Opt) error
Transfer(ctx context.Context, source any, destination any, opts ...Opt) error
}
type ImageResolver interface {
@@ -61,7 +61,7 @@ var (
var (
bufioReader32KPool = &sync.Pool{
New: func() interface{} { return bufio.NewReaderSize(nil, 32*1024) },
New: func() any { return bufio.NewReaderSize(nil, 32*1024) },
}
)
+1 -1
View File
@@ -281,7 +281,7 @@ func (pe parseError) Error() string {
return fmt.Sprintf("[%s]: %v", pe.input, pe.msg)
}
func (p *parser) mkerr(pos int, format string, args ...interface{}) error {
func (p *parser) mkerr(pos int, format string, args ...any) error {
return fmt.Errorf("parse error: %w", parseError{
input: p.input,
pos: pos,
+1 -1
View File
@@ -121,7 +121,7 @@ func unquoteChar(s string, quote byte) (value rune, multibyte bool, tail string,
err = errQuoteSyntax
return
}
for j := 0; j < 2; j++ { // one digit already; two more
for j := range 2 { // one digit already; two more
x := rune(s[j]) - '0'
if x < 0 || x > 7 {
err = errQuoteSyntax
@@ -14,28 +14,25 @@
limitations under the License.
*/
package protobuf
package tracing
import (
"github.com/google/go-cmp/cmp"
"google.golang.org/protobuf/proto"
"context"
"github.com/containerd/containerd/v2/pkg/namespaces"
"go.opentelemetry.io/otel/trace"
)
var Compare = cmp.FilterValues(
func(x, y interface{}) bool {
_, xok := x.(proto.Message)
_, yok := y.(proto.Message)
return xok && yok
},
cmp.Comparer(func(x, y interface{}) bool {
vx, ok := x.(proto.Message)
if !ok {
return false
// WithNamespace adds containerd namespace attribute to spans when available.
// It is best-effort: if namespace is not present in the context, it does nothing.
func WithNamespace(ctx context.Context) SpanOpt {
return func(config *StartConfig) {
ns, err := namespaces.NamespaceRequired(ctx)
if err != nil {
return
}
vy, ok := y.(proto.Message)
if !ok {
return false
}
return proto.Equal(vx, vy)
}),
)
config.spanOpts = append(config.spanOpts,
trace.WithAttributes(Attribute("namespace", ns)),
)
}
}
+26 -4
View File
@@ -35,9 +35,21 @@ var allLevels = []log.Level{
log.TraceLevel,
}
type HookOpt func(*LogrusHook)
// NewLogrusHook creates a new logrus hook
func NewLogrusHook() *LogrusHook {
return &LogrusHook{}
func NewLogrusHook(opts ...HookOpt) *LogrusHook {
hook := &LogrusHook{}
for _, opt := range opts {
opt(hook)
}
return hook
}
func WithTraceIDField(enabled bool) HookOpt {
return func(h *LogrusHook) {
h.enableTraceIDField = enabled
}
}
// LogrusHook is a [logrus.Hook] which adds logrus events to active spans.
@@ -45,7 +57,9 @@ func NewLogrusHook() *LogrusHook {
// is a no-op.
//
// [logrus.Hook]: https://github.com/sirupsen/logrus/blob/v1.9.3/hooks.go#L3-L11
type LogrusHook struct{}
type LogrusHook struct {
enableTraceIDField bool
}
// Levels returns the logrus levels that this hook is interested in.
func (h *LogrusHook) Levels() []log.Level {
@@ -59,7 +73,15 @@ func (h *LogrusHook) Fire(entry *log.Entry) error {
return nil
}
if !span.IsRecording() || !span.SpanContext().IsValid() {
if !span.SpanContext().IsValid() {
return nil
}
if h.enableTraceIDField {
entry.Data["trace_id"] = span.SpanContext().TraceID().String()
}
if !span.IsRecording() {
return nil
}
+1 -1
View File
@@ -36,7 +36,7 @@ type StartConfig struct {
type SpanOpt func(config *StartConfig)
// WithAttribute appends attributes to a new created span.
func WithAttribute(k string, v interface{}) SpanOpt {
func WithAttribute(k string, v any) SpanOpt {
return func(config *StartConfig) {
config.spanOpts = append(config.spanOpts,
trace.WithAttributes(Attribute(k, v)))
+22 -5
View File
@@ -18,8 +18,10 @@ package local
import (
"context"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strconv"
@@ -39,7 +41,7 @@ import (
)
var bufPool = sync.Pool{
New: func() interface{} {
New: func() any {
buffer := make([]byte, 1<<20)
return &buffer
},
@@ -84,8 +86,18 @@ func NewStore(root string) (content.Store, error) {
// require labels and should use `NewStore`. `NewLabeledStore` is primarily
// useful for tests or standalone implementations.
func NewLabeledStore(root string, ls LabelStore) (content.Store, error) {
supported, _ := fsverity.IsSupported(root)
if _, err := os.Stat(root); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return nil, fmt.Errorf("failed to stat %q: %w", root, err)
}
if err := os.MkdirAll(root, 0755); err != nil {
return nil, fmt.Errorf("failed to mkdir %q: %w", root, err)
}
}
supported, err := fsverity.IsSupported(root)
if err != nil {
log.L.WithError(err).WithField("path", root).Warnf("failed check for fsverity support")
}
s := &store{
root: root,
ls: ls,
@@ -533,8 +545,13 @@ func (s *store) writer(ctx context.Context, ref string, total int64, expected di
path, refp, data := s.ingestPaths(ref)
// if we get passed an expected digest, we need to use the same algorithm (sha512, etc)
digestAlg := digest.Canonical
if expected != "" && expected.Algorithm().Available() {
digestAlg = expected.Algorithm()
}
var (
digester = digest.Canonical.Digester()
digester = digestAlg.Digester()
offset int64
startedAt time.Time
updatedAt time.Time
@@ -582,7 +599,7 @@ func (s *store) writer(ctx context.Context, ref string, total int64, expected di
}
if total > 0 {
if err := os.WriteFile(filepath.Join(path, "total"), []byte(fmt.Sprint(total)), 0666); err != nil {
if err := os.WriteFile(filepath.Join(path, "total"), fmt.Append(nil, total), 0666); err != nil {
return nil, err
}
}
@@ -113,6 +113,24 @@ func (w *writer) Commit(ctx context.Context, size int64, expected digest.Digest,
}
dgst := w.digester.Digest()
if expected != "" && expected.Algorithm() != dgst.Algorithm() && expected.Algorithm().Available() {
// Writer was opened without a descriptor specifying the digest algorithm (but we got a non-canonical one here in commit), so we have to re-hash our now completed and closed content to compare
start := time.Now()
f, err := os.Open(filepath.Join(w.path, "data"))
if err != nil {
return fmt.Errorf("failed to open ingest data for re-hashing to %s: %w", expected.Algorithm().String(), err)
}
w.digester = expected.Algorithm().Digester()
_, err = io.Copy(w.digester.Hash(), f)
f.Close()
if err != nil {
return fmt.Errorf("failed to re-hash ingest data to %s: %w", expected.Algorithm().String(), err)
}
dgst = w.digester.Digest()
if duration := time.Since(start); duration > 250*time.Millisecond {
log.G(ctx).WithField("digest", dgst).WithField("duration", duration).Warnf("commit for blob required expensive re-hash")
}
}
if expected != "" && expected != dgst {
return fmt.Errorf("unexpected commit digest %s, expected %s: %w", dgst, expected, errdefs.ErrFailedPrecondition)
}
@@ -44,7 +44,7 @@ type service struct {
var (
empty = &ptypes.Empty{}
bufPool = sync.Pool{
New: func() interface{} {
New: func() any {
buffer := make([]byte, 1<<20)
return &buffer
},
+2 -2
View File
@@ -24,7 +24,7 @@ var (
Package = "github.com/containerd/containerd/v2"
// Version holds the complete version number. Filled in at linking time.
Version = "2.2.5+unknown"
Version = "2.3.3+unknown"
// Revision is filled with the VCS (e.g. git) revision being used to build
// the program at linking time.
@@ -38,4 +38,4 @@ var (
// This version is used by the main configuration as well as all plugins.
// Any configuration less than this version which has structural changes
// should migrate the configuration structures used by this version.
const ConfigVersion = 3
const ConfigVersion = 4
+10 -27
View File
@@ -24,7 +24,7 @@ WHALE = "🇩"
ONI = "👹"
# Project binaries.
COMMANDS=protoc-gen-go-ttrpc protoc-gen-gogottrpc
COMMANDS=protoc-gen-go-ttrpc
ifdef BUILDTAGS
GO_BUILDTAGS = ${BUILDTAGS}
@@ -57,7 +57,7 @@ TESTFLAGS_PARALLEL ?= 8
# Use this to replace `go test` with, for instance, `gotestsum`
GOTEST ?= $(GO) test
.PHONY: clean all AUTHORS build binaries test integration generate protos check-protos coverage ci check help install vendor install-protobuf install-protobuild
.PHONY: clean all AUTHORS build binaries test integration generate protos check-protos coverage ci check help install vendor maintainer-clean
.DEFAULT: default
# Forcibly set the default goal to all, in case an include above brought in a rule definition.
@@ -65,7 +65,7 @@ GOTEST ?= $(GO) test
all: binaries
check: proto-fmt ## run all linters
check: ## run all linters
@echo "$(WHALE) $@"
GOGC=75 golangci-lint run
@@ -78,9 +78,10 @@ generate: protos
@echo "$(WHALE) $@"
@PATH="${ROOTDIR}/bin:${PATH}" $(GO) generate -x ${PACKAGES}
protos: bin/protoc-gen-gogottrpc bin/protoc-gen-go-ttrpc ## generate protobuf
protos: bin/protoc-gen-go-ttrpc ## generate protobuf
@echo "$(WHALE) $@"
@(PATH="${ROOTDIR}/bin:${PATH}" protobuild --quiet ${PACKAGES})
(cd example && buf generate)
buf generate
check-protos: protos ## check if protobufs needs to be generated again
@echo "$(WHALE) $@"
@@ -88,19 +89,6 @@ check-protos: protos ## check if protobufs needs to be generated again
((git diff | cat) && \
(echo "$(ONI) please run 'make protos' when making changes to proto files" && false))
check-api-descriptors: protos ## check that protobuf changes aren't present.
@echo "$(WHALE) $@"
@test -z "$$(git status --short | grep ".pb.txt" | tee /dev/stderr)" || \
((git diff $$(find . -name '*.pb.txt') | cat) && \
(echo "$(ONI) please run 'make protos' when making changes to proto files and check-in the generated descriptor file changes" && false))
proto-fmt: ## check format of proto files
@echo "$(WHALE) $@"
@test -z "$$(find . -name '*.proto' -type f -exec grep -Hn -e "^ " {} \; | tee /dev/stderr)" || \
(echo "$(ONI) please indent proto files with tabs only" && false)
@test -z "$$(find . -name '*.proto' -type f -exec grep -Hn "Meta meta = " {} \; | grep -v '(gogoproto.nullable) = false' | tee /dev/stderr)" || \
(echo "$(ONI) meta fields in proto files must have option (gogoproto.nullable) = false" && false)
build: ## build the go packages
@echo "$(WHALE) $@"
@$(GO) build ${DEBUG_GO_GCFLAGS} ${GO_GCFLAGS} ${GO_BUILD_FLAGS} ${EXTRA_FLAGS} ${PACKAGES}
@@ -117,6 +105,10 @@ benchmark: ## run benchmarks tests
@echo "$(WHALE) $@"
@$(GO) test ${TESTFLAGS} -bench . -run Benchmark
maintainer-clean:
@echo "$(WHALE) $@"
@find . -name '*.pb.go' -delete
FORCE:
define BUILD_BINARY
@@ -139,15 +131,6 @@ install: ## install binaries
@echo "$(WHALE) $@ $(BINPACKAGES)"
@$(GO) install $(BINPACKAGES)
install-protobuf:
@echo "$(WHALE) $@"
@script/install-protobuf
install-protobuild:
@echo "$(WHALE) $@"
@$(GO) install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.1
@$(GO) install github.com/containerd/protobuild@14832ccc41429f5c4f81028e5af08aa233a219cf
coverage: ## generate coverprofiles from the unit tests, except tests that require root
@echo "$(WHALE) $@"
@rm -f coverage.txt
-28
View File
@@ -1,28 +0,0 @@
version = "2"
generators = ["go"]
# Control protoc include paths. Below are usually some good defaults, but feel
# free to try it without them if it works for your project.
[includes]
# Include paths that will be added before all others. Typically, you want to
# treat the root of the project as an include, but this may not be necessary.
before = ["."]
# Paths that will be added untouched to the end of the includes. We use
# `/usr/local/include` to pickup the common install location of protobuf.
# This is the default.
after = ["/usr/local/include"]
# This section maps protobuf imports to Go packages. These will become
# `-M` directives in the call to the go protobuf generator.
[packages]
"google/protobuf/any.proto" = "github.com/gogo/protobuf/types"
"proto/status.proto" = "google.golang.org/genproto/googleapis/rpc/status"
[[overrides]]
# enable ttrpc and disable fieldpath and grpc for the shim
prefixes = ["github.com/containerd/ttrpc/integration/streaming"]
generators = ["go", "go-ttrpc"]
[overrides.parameters.go-ttrpc]
prefix = "TTRPC"
-4
View File
@@ -25,10 +25,6 @@ See the [protocol specification](./PROTOCOL.md).
# Usage
Create a gogo vanity binary (see
[`cmd/protoc-gen-gogottrpc/main.go`](cmd/protoc-gen-gogottrpc/main.go) for an
example with the ttrpc plugin enabled.
It's recommended to use [`protobuild`](https://github.com/containerd/protobuild)
to build the protobufs for this project, but this will work with protoc
directly, if required.
+11
View File
@@ -0,0 +1,11 @@
version: v2
plugins:
- remote: buf.build/protocolbuffers/go:v1.28.1
out: .
opt:
- paths=source_relative
- local: bin/protoc-gen-go-ttrpc
out: .
opt:
- paths=source_relative
- prefix=TTRPC
+6
View File
@@ -0,0 +1,6 @@
# Generated by buf. DO NOT EDIT.
version: v2
deps:
- name: buf.build/googleapis/googleapis
commit: 004180b77378443887d3b55cabc00384
digest: b5:e8f475fe3330f31f5fd86ac689093bcd274e19611a09db91f41d637cb9197881ce89882b94d13a58738e53c91c6e4bae7dc1feba85f590164c975a89e25115dc
+10
View File
@@ -0,0 +1,10 @@
version: v2
deps:
# For google/rpc/status.proto
- buf.build/googleapis/googleapis:004180b77378443887d3b55cabc00384
modules:
- path: .
# example/ has own buf.yaml and buf.gen.yaml to not have
# TTRPC prefix.
excludes:
- example
+4 -4
View File
@@ -386,7 +386,7 @@ func (c *Client) receiveLoop() error {
// createStream creates a new stream and registers it with the client
// Introduce stream types for multiple or single response
func (c *Client) createStream(flags uint8, b []byte) (*stream, error) {
func (c *Client) createStream(flags uint8, b []byte, recvBuf int) (*stream, error) {
// sendLock must be held across both allocation of the stream ID and sending it across the wire.
// This ensures that new stream IDs sent on the wire are always increasing, which is a
// requirement of the TTRPC protocol.
@@ -417,7 +417,7 @@ func (c *Client) createStream(flags uint8, b []byte) (*stream, error) {
default:
}
s = newStream(c.nextStreamID, c)
s = newStream(c.nextStreamID, c, recvBuf)
c.streams[s.id] = s
c.nextStreamID = c.nextStreamID + 2
@@ -517,7 +517,7 @@ func (c *Client) NewStream(ctx context.Context, desc *StreamDesc, service, metho
} else {
flags = flagRemoteClosed
}
s, err := c.createStream(flags, p)
s, err := c.createStream(flags, p, streamRecvBufferSize)
if err != nil {
return nil, err
}
@@ -536,7 +536,7 @@ func (c *Client) dispatch(ctx context.Context, req *Request, resp *Response) err
return err
}
s, err := c.createStream(0, p)
s, err := c.createStream(0, p, 1)
if err != nil {
return err
}
+6
View File
@@ -36,6 +36,12 @@ var (
// ErrStreamClosed is when the streaming connection is closed.
ErrStreamClosed = errors.New("ttrpc: stream closed")
// ErrStreamFull is returned when a stream's receive buffer is full
// and the message cannot be delivered without blocking the
// connection's receive loop. This prevents a single unconsumed
// stream from deadlocking all other streams on the same connection.
ErrStreamFull = errors.New("ttrpc: stream buffer full")
)
// OversizedMessageErr is used to indicate refusal to send an oversized message.
+69 -70
View File
@@ -1,8 +1,8 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.28.1
// protoc v3.20.1
// source: github.com/containerd/ttrpc/request.proto
// protoc (unknown)
// source: request.proto
package ttrpc
@@ -36,7 +36,7 @@ type Request struct {
func (x *Request) Reset() {
*x = Request{}
if protoimpl.UnsafeEnabled {
mi := &file_github_com_containerd_ttrpc_request_proto_msgTypes[0]
mi := &file_request_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -49,7 +49,7 @@ func (x *Request) String() string {
func (*Request) ProtoMessage() {}
func (x *Request) ProtoReflect() protoreflect.Message {
mi := &file_github_com_containerd_ttrpc_request_proto_msgTypes[0]
mi := &file_request_proto_msgTypes[0]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -62,7 +62,7 @@ func (x *Request) ProtoReflect() protoreflect.Message {
// Deprecated: Use Request.ProtoReflect.Descriptor instead.
func (*Request) Descriptor() ([]byte, []int) {
return file_github_com_containerd_ttrpc_request_proto_rawDescGZIP(), []int{0}
return file_request_proto_rawDescGZIP(), []int{0}
}
func (x *Request) GetService() string {
@@ -112,7 +112,7 @@ type Response struct {
func (x *Response) Reset() {
*x = Response{}
if protoimpl.UnsafeEnabled {
mi := &file_github_com_containerd_ttrpc_request_proto_msgTypes[1]
mi := &file_request_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -125,7 +125,7 @@ func (x *Response) String() string {
func (*Response) ProtoMessage() {}
func (x *Response) ProtoReflect() protoreflect.Message {
mi := &file_github_com_containerd_ttrpc_request_proto_msgTypes[1]
mi := &file_request_proto_msgTypes[1]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -138,7 +138,7 @@ func (x *Response) ProtoReflect() protoreflect.Message {
// Deprecated: Use Response.ProtoReflect.Descriptor instead.
func (*Response) Descriptor() ([]byte, []int) {
return file_github_com_containerd_ttrpc_request_proto_rawDescGZIP(), []int{1}
return file_request_proto_rawDescGZIP(), []int{1}
}
func (x *Response) GetStatus() *status.Status {
@@ -166,7 +166,7 @@ type StringList struct {
func (x *StringList) Reset() {
*x = StringList{}
if protoimpl.UnsafeEnabled {
mi := &file_github_com_containerd_ttrpc_request_proto_msgTypes[2]
mi := &file_request_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -179,7 +179,7 @@ func (x *StringList) String() string {
func (*StringList) ProtoMessage() {}
func (x *StringList) ProtoReflect() protoreflect.Message {
mi := &file_github_com_containerd_ttrpc_request_proto_msgTypes[2]
mi := &file_request_proto_msgTypes[2]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -192,7 +192,7 @@ func (x *StringList) ProtoReflect() protoreflect.Message {
// Deprecated: Use StringList.ProtoReflect.Descriptor instead.
func (*StringList) Descriptor() ([]byte, []int) {
return file_github_com_containerd_ttrpc_request_proto_rawDescGZIP(), []int{2}
return file_request_proto_rawDescGZIP(), []int{2}
}
func (x *StringList) GetList() []string {
@@ -214,7 +214,7 @@ type KeyValue struct {
func (x *KeyValue) Reset() {
*x = KeyValue{}
if protoimpl.UnsafeEnabled {
mi := &file_github_com_containerd_ttrpc_request_proto_msgTypes[3]
mi := &file_request_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -227,7 +227,7 @@ func (x *KeyValue) String() string {
func (*KeyValue) ProtoMessage() {}
func (x *KeyValue) ProtoReflect() protoreflect.Message {
mi := &file_github_com_containerd_ttrpc_request_proto_msgTypes[3]
mi := &file_request_proto_msgTypes[3]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -240,7 +240,7 @@ func (x *KeyValue) ProtoReflect() protoreflect.Message {
// Deprecated: Use KeyValue.ProtoReflect.Descriptor instead.
func (*KeyValue) Descriptor() ([]byte, []int) {
return file_github_com_containerd_ttrpc_request_proto_rawDescGZIP(), []int{3}
return file_request_proto_rawDescGZIP(), []int{3}
}
func (x *KeyValue) GetKey() string {
@@ -257,62 +257,61 @@ func (x *KeyValue) GetValue() string {
return ""
}
var File_github_com_containerd_ttrpc_request_proto protoreflect.FileDescriptor
var File_request_proto protoreflect.FileDescriptor
var file_github_com_containerd_ttrpc_request_proto_rawDesc = []byte{
0x0a, 0x29, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x6e,
0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x2f, 0x74, 0x74, 0x72, 0x70, 0x63, 0x2f, 0x72, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x74, 0x74, 0x72,
0x70, 0x63, 0x1a, 0x12, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xa5, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06,
0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65,
0x74, 0x68, 0x6f, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18,
0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x21,
0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x5f, 0x6e, 0x61, 0x6e, 0x6f, 0x18, 0x04,
0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x4e, 0x61, 0x6e,
0x6f, 0x12, 0x2b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x74, 0x74, 0x72, 0x70, 0x63, 0x2e, 0x4b, 0x65, 0x79, 0x56,
0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x45,
0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1f, 0x0a, 0x06, 0x73, 0x74,
0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x07, 0x2e, 0x53, 0x74, 0x61,
0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x70,
0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x70, 0x61,
0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x20, 0x0a, 0x0a, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4c,
0x69, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x18, 0x01, 0x20, 0x03, 0x28,
0x09, 0x52, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x22, 0x32, 0x0a, 0x08, 0x4b, 0x65, 0x79, 0x56, 0x61,
0x6c, 0x75, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02,
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x1d, 0x5a, 0x1b, 0x67,
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69,
0x6e, 0x65, 0x72, 0x64, 0x2f, 0x74, 0x74, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
var file_request_proto_rawDesc = []byte{
0x0a, 0x0d, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
0x05, 0x74, 0x74, 0x72, 0x70, 0x63, 0x1a, 0x17, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x72,
0x70, 0x63, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22,
0xa5, 0x01, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x65,
0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18,
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x18, 0x0a,
0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07,
0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x6f,
0x75, 0x74, 0x5f, 0x6e, 0x61, 0x6e, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x74,
0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x4e, 0x61, 0x6e, 0x6f, 0x12, 0x2b, 0x0a, 0x08, 0x6d, 0x65,
0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x74,
0x74, 0x72, 0x70, 0x63, 0x2e, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x08, 0x6d,
0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x50, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x12, 0x2a, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20,
0x01, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x72, 0x70, 0x63,
0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12,
0x18, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c,
0x52, 0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x20, 0x0a, 0x0a, 0x53, 0x74, 0x72,
0x69, 0x6e, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x18,
0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x6c, 0x69, 0x73, 0x74, 0x22, 0x32, 0x0a, 0x08, 0x4b,
0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c,
0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x42,
0x1d, 0x5a, 0x1b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f,
0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x2f, 0x74, 0x74, 0x72, 0x70, 0x63, 0x62, 0x06,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_github_com_containerd_ttrpc_request_proto_rawDescOnce sync.Once
file_github_com_containerd_ttrpc_request_proto_rawDescData = file_github_com_containerd_ttrpc_request_proto_rawDesc
file_request_proto_rawDescOnce sync.Once
file_request_proto_rawDescData = file_request_proto_rawDesc
)
func file_github_com_containerd_ttrpc_request_proto_rawDescGZIP() []byte {
file_github_com_containerd_ttrpc_request_proto_rawDescOnce.Do(func() {
file_github_com_containerd_ttrpc_request_proto_rawDescData = protoimpl.X.CompressGZIP(file_github_com_containerd_ttrpc_request_proto_rawDescData)
func file_request_proto_rawDescGZIP() []byte {
file_request_proto_rawDescOnce.Do(func() {
file_request_proto_rawDescData = protoimpl.X.CompressGZIP(file_request_proto_rawDescData)
})
return file_github_com_containerd_ttrpc_request_proto_rawDescData
return file_request_proto_rawDescData
}
var file_github_com_containerd_ttrpc_request_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_github_com_containerd_ttrpc_request_proto_goTypes = []interface{}{
var file_request_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
var file_request_proto_goTypes = []interface{}{
(*Request)(nil), // 0: ttrpc.Request
(*Response)(nil), // 1: ttrpc.Response
(*StringList)(nil), // 2: ttrpc.StringList
(*KeyValue)(nil), // 3: ttrpc.KeyValue
(*status.Status)(nil), // 4: Status
(*status.Status)(nil), // 4: google.rpc.Status
}
var file_github_com_containerd_ttrpc_request_proto_depIdxs = []int32{
var file_request_proto_depIdxs = []int32{
3, // 0: ttrpc.Request.metadata:type_name -> ttrpc.KeyValue
4, // 1: ttrpc.Response.status:type_name -> Status
4, // 1: ttrpc.Response.status:type_name -> google.rpc.Status
2, // [2:2] is the sub-list for method output_type
2, // [2:2] is the sub-list for method input_type
2, // [2:2] is the sub-list for extension type_name
@@ -320,13 +319,13 @@ var file_github_com_containerd_ttrpc_request_proto_depIdxs = []int32{
0, // [0:2] is the sub-list for field type_name
}
func init() { file_github_com_containerd_ttrpc_request_proto_init() }
func file_github_com_containerd_ttrpc_request_proto_init() {
if File_github_com_containerd_ttrpc_request_proto != nil {
func init() { file_request_proto_init() }
func file_request_proto_init() {
if File_request_proto != nil {
return
}
if !protoimpl.UnsafeEnabled {
file_github_com_containerd_ttrpc_request_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
file_request_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Request); i {
case 0:
return &v.state
@@ -338,7 +337,7 @@ func file_github_com_containerd_ttrpc_request_proto_init() {
return nil
}
}
file_github_com_containerd_ttrpc_request_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
file_request_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Response); i {
case 0:
return &v.state
@@ -350,7 +349,7 @@ func file_github_com_containerd_ttrpc_request_proto_init() {
return nil
}
}
file_github_com_containerd_ttrpc_request_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
file_request_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*StringList); i {
case 0:
return &v.state
@@ -362,7 +361,7 @@ func file_github_com_containerd_ttrpc_request_proto_init() {
return nil
}
}
file_github_com_containerd_ttrpc_request_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
file_request_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*KeyValue); i {
case 0:
return &v.state
@@ -379,18 +378,18 @@ func file_github_com_containerd_ttrpc_request_proto_init() {
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_github_com_containerd_ttrpc_request_proto_rawDesc,
RawDescriptor: file_request_proto_rawDesc,
NumEnums: 0,
NumMessages: 4,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_github_com_containerd_ttrpc_request_proto_goTypes,
DependencyIndexes: file_github_com_containerd_ttrpc_request_proto_depIdxs,
MessageInfos: file_github_com_containerd_ttrpc_request_proto_msgTypes,
GoTypes: file_request_proto_goTypes,
DependencyIndexes: file_request_proto_depIdxs,
MessageInfos: file_request_proto_msgTypes,
}.Build()
File_github_com_containerd_ttrpc_request_proto = out.File
file_github_com_containerd_ttrpc_request_proto_rawDesc = nil
file_github_com_containerd_ttrpc_request_proto_goTypes = nil
file_github_com_containerd_ttrpc_request_proto_depIdxs = nil
File_request_proto = out.File
file_request_proto_rawDesc = nil
file_request_proto_goTypes = nil
file_request_proto_depIdxs = nil
}
+2 -2
View File
@@ -2,7 +2,7 @@ syntax = "proto3";
package ttrpc;
import "proto/status.proto";
import "google/rpc/status.proto";
option go_package = "github.com/containerd/ttrpc";
@@ -15,7 +15,7 @@ message Request {
}
message Response {
Status status = 1;
google.rpc.Status status = 1;
bytes payload = 2;
}
+3 -6
View File
@@ -568,8 +568,6 @@ func (c *serverConn) run(sctx context.Context) {
}
}
var noopFunc = func() {}
func getRequestContext(ctx context.Context, req *Request) (retCtx context.Context, cancel func()) {
if len(req.Metadata) > 0 {
md := MD{}
@@ -577,11 +575,10 @@ func getRequestContext(ctx context.Context, req *Request) (retCtx context.Contex
ctx = WithMetadata(ctx, md)
}
cancel = noopFunc
if req.TimeoutNano == 0 {
return ctx, cancel
// Cancellable so handlers' deferred cancel propagates to RecvMsg.
return context.WithCancel(ctx)
}
ctx, cancel = context.WithTimeout(ctx, time.Duration(req.TimeoutNano))
return ctx, cancel
return context.WithTimeout(ctx, time.Duration(req.TimeoutNano))
}
+23 -1
View File
@@ -23,6 +23,7 @@ import (
"io"
"os"
"path"
"time"
"unsafe"
"google.golang.org/grpc/codes"
@@ -128,10 +129,14 @@ func (s *serviceSet) handle(ctx context.Context, req *Request, respond func(*sta
StreamingClient: stream.StreamingClient,
StreamingServer: stream.StreamingServer,
}
recvBuf := streamRecvBufferSize
if !stream.StreamingClient {
recvBuf = 1
}
sh := &streamHandler{
ctx: ctx,
respond: respond,
recv: make(chan Unmarshaler, 5),
recv: make(chan Unmarshaler, recvBuf),
info: info,
}
go func() {
@@ -158,6 +163,12 @@ func (s *serviceSet) handle(ctx context.Context, req *Request, respond func(*sta
return nil, status.Errorf(codes.Unimplemented, "method %v", req.Method)
}
// streamRecvBufferSize is the buffer size for stream recv channels. It
// should be large enough to absorb normal bursts without hitting the
// 1-second timeout fallback in receive/data, but small enough that
// per-stream memory overhead stays trivial.
const streamRecvBufferSize = 64
type streamHandler struct {
ctx context.Context
respond func(*status.Status, []byte, bool, bool) error
@@ -184,6 +195,17 @@ func (s *streamHandler) data(unmarshal Unmarshaler) error {
return nil
case <-s.ctx.Done():
return s.ctx.Err()
default:
// If recv channel is full, wait up to a second for an item
// to drain and unblock, otherwise return an error.
select {
case s.recv <- unmarshal:
return nil
case <-s.ctx.Done():
return s.ctx.Err()
case <-time.After(time.Second):
return ErrStreamFull
}
}
}
+22 -2
View File
@@ -19,6 +19,7 @@ package ttrpc
import (
"context"
"sync"
"time"
)
type streamID uint32
@@ -38,11 +39,11 @@ type stream struct {
recvClose chan struct{}
}
func newStream(id streamID, send sender) *stream {
func newStream(id streamID, send sender, recvBuf int) *stream {
return &stream{
id: id,
sender: send,
recv: make(chan *streamMessage, 1),
recv: make(chan *streamMessage, recvBuf),
recvClose: make(chan struct{}),
}
}
@@ -63,6 +64,11 @@ func (s *stream) send(mt messageType, flags uint8, b []byte) error {
return s.sender.send(uint32(s.id), mt, flags, b)
}
// receive delivers a message to this stream from the connection receive loop.
// If the stream's recv buffer is full, it waits up to 1 second for the
// consumer to make progress. This keeps the receive loop moving for other
// streams while still providing backpressure under normal operation. If the
// timeout expires the stream is closed with ErrStreamFull.
func (s *stream) receive(ctx context.Context, msg *streamMessage) error {
select {
case <-s.recvClose:
@@ -76,6 +82,20 @@ func (s *stream) receive(ctx context.Context, msg *streamMessage) error {
return nil
case <-ctx.Done():
return ctx.Err()
default:
// If recv channel is full, wait up to a second for an item
// to drain and unblock, otherwise close the stream.
select {
case <-s.recvClose:
return s.recvErr
case s.recv <- msg:
return nil
case <-ctx.Done():
return ctx.Err()
case <-time.After(time.Second):
s.closeWithError(ErrStreamFull)
return ErrStreamFull
}
}
}
-16
View File
@@ -1,16 +0,0 @@
syntax = "proto3";
package ttrpc;
option go_package = "github.com/containerd/ttrpc/internal";
message TestPayload {
string foo = 1;
int64 deadline = 2;
string metadata = 3;
}
message EchoPayload {
int64 seq = 1;
string msg = 2;
}