1/*
2   Copyright The containerd Authors.
3
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7
8       http://www.apache.org/licenses/LICENSE-2.0
9
10   Unless required by applicable law or agreed to in writing, software
11   distributed under the License is distributed on an "AS IS" BASIS,
12   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   See the License for the specific language governing permissions and
14   limitations under the License.
15*/
16
17package images
18
19import (
20	"context"
21	"fmt"
22	"sort"
23
24	"github.com/containerd/containerd/content"
25	"github.com/containerd/containerd/errdefs"
26	"github.com/containerd/containerd/platforms"
27	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
28	"github.com/pkg/errors"
29	"golang.org/x/sync/errgroup"
30	"golang.org/x/sync/semaphore"
31)
32
33var (
34	// ErrSkipDesc is used to skip processing of a descriptor and
35	// its descendants.
36	ErrSkipDesc = fmt.Errorf("skip descriptor")
37
38	// ErrStopHandler is used to signify that the descriptor
39	// has been handled and should not be handled further.
40	// This applies only to a single descriptor in a handler
41	// chain and does not apply to descendant descriptors.
42	ErrStopHandler = fmt.Errorf("stop handler")
43)
44
45// Handler handles image manifests
46type Handler interface {
47	Handle(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error)
48}
49
50// HandlerFunc function implementing the Handler interface
51type HandlerFunc func(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error)
52
53// Handle image manifests
54func (fn HandlerFunc) Handle(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error) {
55	return fn(ctx, desc)
56}
57
58// Handlers returns a handler that will run the handlers in sequence.
59//
60// A handler may return `ErrStopHandler` to stop calling additional handlers
61func Handlers(handlers ...Handler) HandlerFunc {
62	return func(ctx context.Context, desc ocispec.Descriptor) (subdescs []ocispec.Descriptor, err error) {
63		var children []ocispec.Descriptor
64		for _, handler := range handlers {
65			ch, err := handler.Handle(ctx, desc)
66			if err != nil {
67				if errors.Is(err, ErrStopHandler) {
68					break
69				}
70				return nil, err
71			}
72
73			children = append(children, ch...)
74		}
75
76		return children, nil
77	}
78}
79
80// Walk the resources of an image and call the handler for each. If the handler
81// decodes the sub-resources for each image,
82//
83// This differs from dispatch in that each sibling resource is considered
84// synchronously.
85func Walk(ctx context.Context, handler Handler, descs ...ocispec.Descriptor) error {
86	for _, desc := range descs {
87
88		children, err := handler.Handle(ctx, desc)
89		if err != nil {
90			if errors.Is(err, ErrSkipDesc) {
91				continue // don't traverse the children.
92			}
93			return err
94		}
95
96		if len(children) > 0 {
97			if err := Walk(ctx, handler, children...); err != nil {
98				return err
99			}
100		}
101	}
102
103	return nil
104}
105
106// Dispatch runs the provided handler for content specified by the descriptors.
107// If the handler decode subresources, they will be visited, as well.
108//
109// Handlers for siblings are run in parallel on the provided descriptors. A
110// handler may return `ErrSkipDesc` to signal to the dispatcher to not traverse
111// any children.
112//
113// A concurrency limiter can be passed in to limit the number of concurrent
114// handlers running. When limiter is nil, there is no limit.
115//
116// Typically, this function will be used with `FetchHandler`, often composed
117// with other handlers.
118//
119// If any handler returns an error, the dispatch session will be canceled.
120func Dispatch(ctx context.Context, handler Handler, limiter *semaphore.Weighted, descs ...ocispec.Descriptor) error {
121	eg, ctx2 := errgroup.WithContext(ctx)
122	for _, desc := range descs {
123		desc := desc
124
125		if limiter != nil {
126			if err := limiter.Acquire(ctx, 1); err != nil {
127				return err
128			}
129		}
130
131		eg.Go(func() error {
132			desc := desc
133
134			children, err := handler.Handle(ctx2, desc)
135			if limiter != nil {
136				limiter.Release(1)
137			}
138			if err != nil {
139				if errors.Is(err, ErrSkipDesc) {
140					return nil // don't traverse the children.
141				}
142				return err
143			}
144
145			if len(children) > 0 {
146				return Dispatch(ctx2, handler, limiter, children...)
147			}
148
149			return nil
150		})
151	}
152
153	return eg.Wait()
154}
155
156// ChildrenHandler decodes well-known manifest types and returns their children.
157//
158// This is useful for supporting recursive fetch and other use cases where you
159// want to do a full walk of resources.
160//
161// One can also replace this with another implementation to allow descending of
162// arbitrary types.
163func ChildrenHandler(provider content.Provider) HandlerFunc {
164	return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
165		return Children(ctx, provider, desc)
166	}
167}
168
169// SetChildrenLabels is a handler wrapper which sets labels for the content on
170// the children returned by the handler and passes through the children.
171// Must follow a handler that returns the children to be labeled.
172func SetChildrenLabels(manager content.Manager, f HandlerFunc) HandlerFunc {
173	return SetChildrenMappedLabels(manager, f, nil)
174}
175
176// SetChildrenMappedLabels is a handler wrapper which sets labels for the content on
177// the children returned by the handler and passes through the children.
178// Must follow a handler that returns the children to be labeled.
179// The label map allows the caller to control the labels per child descriptor.
180// For returned labels, the index of the child will be appended to the end
181// except for the first index when the returned label does not end with '.'.
182func SetChildrenMappedLabels(manager content.Manager, f HandlerFunc, labelMap func(ocispec.Descriptor) []string) HandlerFunc {
183	if labelMap == nil {
184		labelMap = ChildGCLabels
185	}
186	return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
187		children, err := f(ctx, desc)
188		if err != nil {
189			return children, err
190		}
191
192		if len(children) > 0 {
193			var (
194				info = content.Info{
195					Digest: desc.Digest,
196					Labels: map[string]string{},
197				}
198				fields = []string{}
199				keys   = map[string]uint{}
200			)
201			for _, ch := range children {
202				labelKeys := labelMap(ch)
203				for _, key := range labelKeys {
204					idx := keys[key]
205					keys[key] = idx + 1
206					if idx > 0 || key[len(key)-1] == '.' {
207						key = fmt.Sprintf("%s%d", key, idx)
208					}
209
210					info.Labels[key] = ch.Digest.String()
211					fields = append(fields, "labels."+key)
212				}
213			}
214
215			_, err := manager.Update(ctx, info, fields...)
216			if err != nil {
217				return nil, err
218			}
219		}
220
221		return children, err
222	}
223}
224
225// FilterPlatforms is a handler wrapper which limits the descriptors returned
226// based on matching the specified platform matcher.
227func FilterPlatforms(f HandlerFunc, m platforms.Matcher) HandlerFunc {
228	return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
229		children, err := f(ctx, desc)
230		if err != nil {
231			return children, err
232		}
233
234		var descs []ocispec.Descriptor
235
236		if m == nil {
237			descs = children
238		} else {
239			for _, d := range children {
240				if d.Platform == nil || m.Match(*d.Platform) {
241					descs = append(descs, d)
242				}
243			}
244		}
245
246		return descs, nil
247	}
248}
249
250// LimitManifests is a handler wrapper which filters the manifest descriptors
251// returned using the provided platform.
252// The results will be ordered according to the comparison operator and
253// use the ordering in the manifests for equal matches.
254// A limit of 0 or less is considered no limit.
255// A not found error is returned if no manifest is matched.
256func LimitManifests(f HandlerFunc, m platforms.MatchComparer, n int) HandlerFunc {
257	return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
258		children, err := f(ctx, desc)
259		if err != nil {
260			return children, err
261		}
262
263		switch desc.MediaType {
264		case ocispec.MediaTypeImageIndex, MediaTypeDockerSchema2ManifestList:
265			sort.SliceStable(children, func(i, j int) bool {
266				if children[i].Platform == nil {
267					return false
268				}
269				if children[j].Platform == nil {
270					return true
271				}
272				return m.Less(*children[i].Platform, *children[j].Platform)
273			})
274
275			if n > 0 {
276				if len(children) == 0 {
277					return children, errors.Wrap(errdefs.ErrNotFound, "no match for platform in manifest")
278				}
279				if len(children) > n {
280					children = children[:n]
281				}
282			}
283		default:
284			// only limit manifests from an index
285		}
286		return children, nil
287	}
288}
289