package images import ( "context" "encoding/json" "fmt" "github.com/containerd/containerd/content" "github.com/containerd/containerd/log" "github.com/containerd/containerd/platforms" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "golang.org/x/sync/errgroup" ) var ( // SkipDesc is used to skip processing of a descriptor and // its descendants. SkipDesc = fmt.Errorf("skip descriptor") // StopHandler is used to signify that the descriptor // has been handled and should not be handled further. // This applies only to a single descriptor in a handler // chain and does not apply to descendant descriptors. StopHandler = fmt.Errorf("stop handler") ) type Handler interface { Handle(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error) } type HandlerFunc func(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error) func (fn HandlerFunc) Handle(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error) { return fn(ctx, desc) } // Handlers returns a handler that will run the handlers in sequence. // // A handler may return `StopHandler` to stop calling additional handlers func Handlers(handlers ...Handler) HandlerFunc { return func(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error) { var children []ocispec.Descriptor for _, handler := range handlers { ch, err := handler.Handle(ctx, desc) if err != nil { if errors.Cause(err) == StopHandler { break } return nil, err } children = append(children, ch...) } return children, nil } } // Walk the resources of an image and call the handler for each. If the handler // decodes the sub-resources for each image, // // This differs from dispatch in that each sibling resource is considered // synchronously. func Walk(ctx context.Context, handler Handler, descs ...ocispec.Descriptor) error { for _, desc := range descs { children, err := handler.Handle(ctx, desc) if err != nil { if errors.Cause(err) == SkipDesc { continue // don't traverse the children. } return err } if len(children) > 0 { if err := Walk(ctx, handler, children...); err != nil { return err } } } return nil } // Dispatch runs the provided handler for content specified by the descriptors. // If the handler decode subresources, they will be visited, as well. // // Handlers for siblings are run in parallel on the provided descriptors. A // handler may return `SkipDesc` to signal to the dispatcher to not traverse // any children. // // Typically, this function will be used with `FetchHandler`, often composed // with other handlers. // // If any handler returns an error, the dispatch session will be canceled. func Dispatch(ctx context.Context, handler Handler, descs ...ocispec.Descriptor) error { eg, ctx := errgroup.WithContext(ctx) for _, desc := range descs { desc := desc eg.Go(func() error { desc := desc children, err := handler.Handle(ctx, desc) if err != nil { if errors.Cause(err) == SkipDesc { return nil // don't traverse the children. } return err } if len(children) > 0 { return Dispatch(ctx, handler, children...) } return nil }) } return eg.Wait() } // ChildrenHandler decodes well-known manifests types and returns their children. // // This is useful for supporting recursive fetch and other use cases where you // want to do a full walk of resources. // // One can also replace this with another implementation to allow descending of // arbitrary types. func ChildrenHandler(provider content.Provider, platform string) HandlerFunc { return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) { var descs []ocispec.Descriptor switch desc.MediaType { case MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest: p, err := content.ReadBlob(ctx, provider, desc.Digest) if err != nil { return nil, err } // TODO(stevvooe): We just assume oci manifest, for now. There may be // subtle differences from the docker version. var manifest ocispec.Manifest if err := json.Unmarshal(p, &manifest); err != nil { return nil, err } descs = append(descs, manifest.Config) descs = append(descs, manifest.Layers...) case MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex: p, err := content.ReadBlob(ctx, provider, desc.Digest) if err != nil { return nil, err } var index ocispec.Index if err := json.Unmarshal(p, &index); err != nil { return nil, err } if platform != "" { matcher, err := platforms.Parse(platform) if err != nil { return nil, err } for _, d := range index.Manifests { if d.Platform == nil || matcher.Match(*d.Platform) { descs = append(descs, d) } } } else { descs = append(descs, index.Manifests...) } case MediaTypeDockerSchema2Layer, MediaTypeDockerSchema2LayerGzip, MediaTypeDockerSchema2Config, ocispec.MediaTypeImageConfig, ocispec.MediaTypeImageLayer, ocispec.MediaTypeImageLayerGzip: // childless data types. return nil, nil default: log.G(ctx).Warnf("encountered unknown type %v; children may not be fetched", desc.MediaType) } return descs, nil } }