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 containerd
18
19import (
20	"bytes"
21	"context"
22	"encoding/json"
23	"fmt"
24	"net/http"
25	"runtime"
26	"strconv"
27	"strings"
28	"sync"
29	"time"
30
31	containersapi "github.com/containerd/containerd/api/services/containers/v1"
32	contentapi "github.com/containerd/containerd/api/services/content/v1"
33	diffapi "github.com/containerd/containerd/api/services/diff/v1"
34	eventsapi "github.com/containerd/containerd/api/services/events/v1"
35	imagesapi "github.com/containerd/containerd/api/services/images/v1"
36	introspectionapi "github.com/containerd/containerd/api/services/introspection/v1"
37	leasesapi "github.com/containerd/containerd/api/services/leases/v1"
38	namespacesapi "github.com/containerd/containerd/api/services/namespaces/v1"
39	snapshotsapi "github.com/containerd/containerd/api/services/snapshots/v1"
40	"github.com/containerd/containerd/api/services/tasks/v1"
41	versionservice "github.com/containerd/containerd/api/services/version/v1"
42	"github.com/containerd/containerd/containers"
43	"github.com/containerd/containerd/content"
44	contentproxy "github.com/containerd/containerd/content/proxy"
45	"github.com/containerd/containerd/defaults"
46	"github.com/containerd/containerd/errdefs"
47	"github.com/containerd/containerd/events"
48	"github.com/containerd/containerd/images"
49	"github.com/containerd/containerd/leases"
50	leasesproxy "github.com/containerd/containerd/leases/proxy"
51	"github.com/containerd/containerd/namespaces"
52	"github.com/containerd/containerd/pkg/dialer"
53	"github.com/containerd/containerd/platforms"
54	"github.com/containerd/containerd/plugin"
55	"github.com/containerd/containerd/remotes"
56	"github.com/containerd/containerd/remotes/docker"
57	"github.com/containerd/containerd/services/introspection"
58	"github.com/containerd/containerd/snapshots"
59	snproxy "github.com/containerd/containerd/snapshots/proxy"
60	"github.com/containerd/typeurl"
61	ptypes "github.com/gogo/protobuf/types"
62	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
63	specs "github.com/opencontainers/runtime-spec/specs-go"
64	"github.com/pkg/errors"
65	"google.golang.org/grpc"
66	"google.golang.org/grpc/backoff"
67	"google.golang.org/grpc/health/grpc_health_v1"
68)
69
70func init() {
71	const prefix = "types.containerd.io"
72	// register TypeUrls for commonly marshaled external types
73	major := strconv.Itoa(specs.VersionMajor)
74	typeurl.Register(&specs.Spec{}, prefix, "opencontainers/runtime-spec", major, "Spec")
75	typeurl.Register(&specs.Process{}, prefix, "opencontainers/runtime-spec", major, "Process")
76	typeurl.Register(&specs.LinuxResources{}, prefix, "opencontainers/runtime-spec", major, "LinuxResources")
77	typeurl.Register(&specs.WindowsResources{}, prefix, "opencontainers/runtime-spec", major, "WindowsResources")
78}
79
80// New returns a new containerd client that is connected to the containerd
81// instance provided by address
82func New(address string, opts ...ClientOpt) (*Client, error) {
83	var copts clientOpts
84	for _, o := range opts {
85		if err := o(&copts); err != nil {
86			return nil, err
87		}
88	}
89	if copts.timeout == 0 {
90		copts.timeout = 10 * time.Second
91	}
92
93	c := &Client{
94		defaultns: copts.defaultns,
95	}
96
97	if copts.defaultRuntime != "" {
98		c.runtime = copts.defaultRuntime
99	} else {
100		c.runtime = defaults.DefaultRuntime
101	}
102
103	if copts.defaultPlatform != nil {
104		c.platform = copts.defaultPlatform
105	} else {
106		c.platform = platforms.Default()
107	}
108
109	if copts.services != nil {
110		c.services = *copts.services
111	}
112	if address != "" {
113		backoffConfig := backoff.DefaultConfig
114		backoffConfig.MaxDelay = 3 * time.Second
115		connParams := grpc.ConnectParams{
116			Backoff: backoffConfig,
117		}
118		gopts := []grpc.DialOption{
119			grpc.WithBlock(),
120			grpc.WithInsecure(),
121			grpc.FailOnNonTempDialError(true),
122			grpc.WithConnectParams(connParams),
123			grpc.WithContextDialer(dialer.ContextDialer),
124
125			// TODO(stevvooe): We may need to allow configuration of this on the client.
126			grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(defaults.DefaultMaxRecvMsgSize)),
127			grpc.WithDefaultCallOptions(grpc.MaxCallSendMsgSize(defaults.DefaultMaxSendMsgSize)),
128		}
129		if len(copts.dialOptions) > 0 {
130			gopts = copts.dialOptions
131		}
132		if copts.defaultns != "" {
133			unary, stream := newNSInterceptors(copts.defaultns)
134			gopts = append(gopts,
135				grpc.WithUnaryInterceptor(unary),
136				grpc.WithStreamInterceptor(stream),
137			)
138		}
139		connector := func() (*grpc.ClientConn, error) {
140			ctx, cancel := context.WithTimeout(context.Background(), copts.timeout)
141			defer cancel()
142			conn, err := grpc.DialContext(ctx, dialer.DialAddress(address), gopts...)
143			if err != nil {
144				return nil, errors.Wrapf(err, "failed to dial %q", address)
145			}
146			return conn, nil
147		}
148		conn, err := connector()
149		if err != nil {
150			return nil, err
151		}
152		c.conn, c.connector = conn, connector
153	}
154	if copts.services == nil && c.conn == nil {
155		return nil, errors.Wrap(errdefs.ErrUnavailable, "no grpc connection or services is available")
156	}
157
158	// check namespace labels for default runtime
159	if copts.defaultRuntime == "" && c.defaultns != "" {
160		if label, err := c.GetLabel(context.Background(), defaults.DefaultRuntimeNSLabel); err != nil {
161			return nil, err
162		} else if label != "" {
163			c.runtime = label
164		}
165	}
166
167	return c, nil
168}
169
170// NewWithConn returns a new containerd client that is connected to the containerd
171// instance provided by the connection
172func NewWithConn(conn *grpc.ClientConn, opts ...ClientOpt) (*Client, error) {
173	var copts clientOpts
174	for _, o := range opts {
175		if err := o(&copts); err != nil {
176			return nil, err
177		}
178	}
179	c := &Client{
180		defaultns: copts.defaultns,
181		conn:      conn,
182		runtime:   fmt.Sprintf("%s.%s", plugin.RuntimePlugin, runtime.GOOS),
183	}
184
185	// check namespace labels for default runtime
186	if copts.defaultRuntime == "" && c.defaultns != "" {
187		if label, err := c.GetLabel(context.Background(), defaults.DefaultRuntimeNSLabel); err != nil {
188			return nil, err
189		} else if label != "" {
190			c.runtime = label
191		}
192	}
193
194	if copts.services != nil {
195		c.services = *copts.services
196	}
197	return c, nil
198}
199
200// Client is the client to interact with containerd and its various services
201// using a uniform interface
202type Client struct {
203	services
204	connMu    sync.Mutex
205	conn      *grpc.ClientConn
206	runtime   string
207	defaultns string
208	platform  platforms.MatchComparer
209	connector func() (*grpc.ClientConn, error)
210}
211
212// Reconnect re-establishes the GRPC connection to the containerd daemon
213func (c *Client) Reconnect() error {
214	if c.connector == nil {
215		return errors.Wrap(errdefs.ErrUnavailable, "unable to reconnect to containerd, no connector available")
216	}
217	c.connMu.Lock()
218	defer c.connMu.Unlock()
219	c.conn.Close()
220	conn, err := c.connector()
221	if err != nil {
222		return err
223	}
224	c.conn = conn
225	return nil
226}
227
228// IsServing returns true if the client can successfully connect to the
229// containerd daemon and the healthcheck service returns the SERVING
230// response.
231// This call will block if a transient error is encountered during
232// connection. A timeout can be set in the context to ensure it returns
233// early.
234func (c *Client) IsServing(ctx context.Context) (bool, error) {
235	c.connMu.Lock()
236	if c.conn == nil {
237		c.connMu.Unlock()
238		return false, errors.Wrap(errdefs.ErrUnavailable, "no grpc connection available")
239	}
240	c.connMu.Unlock()
241	r, err := c.HealthService().Check(ctx, &grpc_health_v1.HealthCheckRequest{}, grpc.WaitForReady(true))
242	if err != nil {
243		return false, err
244	}
245	return r.Status == grpc_health_v1.HealthCheckResponse_SERVING, nil
246}
247
248// Containers returns all containers created in containerd
249func (c *Client) Containers(ctx context.Context, filters ...string) ([]Container, error) {
250	r, err := c.ContainerService().List(ctx, filters...)
251	if err != nil {
252		return nil, err
253	}
254	var out []Container
255	for _, container := range r {
256		out = append(out, containerFromRecord(c, container))
257	}
258	return out, nil
259}
260
261// NewContainer will create a new container in container with the provided id
262// the id must be unique within the namespace
263func (c *Client) NewContainer(ctx context.Context, id string, opts ...NewContainerOpts) (Container, error) {
264	ctx, done, err := c.WithLease(ctx)
265	if err != nil {
266		return nil, err
267	}
268	defer done(ctx)
269
270	container := containers.Container{
271		ID: id,
272		Runtime: containers.RuntimeInfo{
273			Name: c.runtime,
274		},
275	}
276	for _, o := range opts {
277		if err := o(ctx, c, &container); err != nil {
278			return nil, err
279		}
280	}
281	r, err := c.ContainerService().Create(ctx, container)
282	if err != nil {
283		return nil, err
284	}
285	return containerFromRecord(c, r), nil
286}
287
288// LoadContainer loads an existing container from metadata
289func (c *Client) LoadContainer(ctx context.Context, id string) (Container, error) {
290	r, err := c.ContainerService().Get(ctx, id)
291	if err != nil {
292		return nil, err
293	}
294	return containerFromRecord(c, r), nil
295}
296
297// RemoteContext is used to configure object resolutions and transfers with
298// remote content stores and image providers.
299type RemoteContext struct {
300	// Resolver is used to resolve names to objects, fetchers, and pushers.
301	// If no resolver is provided, defaults to Docker registry resolver.
302	Resolver remotes.Resolver
303
304	// PlatformMatcher is used to match the platforms for an image
305	// operation and define the preference when a single match is required
306	// from multiple platforms.
307	PlatformMatcher platforms.MatchComparer
308
309	// Unpack is done after an image is pulled to extract into a snapshotter.
310	// It is done simultaneously for schema 2 images when they are pulled.
311	// If an image is not unpacked on pull, it can be unpacked any time
312	// afterwards. Unpacking is required to run an image.
313	Unpack bool
314
315	// UnpackOpts handles options to the unpack call.
316	UnpackOpts []UnpackOpt
317
318	// Snapshotter used for unpacking
319	Snapshotter string
320
321	// SnapshotterOpts are additional options to be passed to a snapshotter during pull
322	SnapshotterOpts []snapshots.Opt
323
324	// Labels to be applied to the created image
325	Labels map[string]string
326
327	// BaseHandlers are a set of handlers which get are called on dispatch.
328	// These handlers always get called before any operation specific
329	// handlers.
330	BaseHandlers []images.Handler
331
332	// HandlerWrapper wraps the handler which gets sent to dispatch.
333	// Unlike BaseHandlers, this can run before and after the built
334	// in handlers, allowing operations to run on the descriptor
335	// after it has completed transferring.
336	HandlerWrapper func(images.Handler) images.Handler
337
338	// ConvertSchema1 is whether to convert Docker registry schema 1
339	// manifests. If this option is false then any image which resolves
340	// to schema 1 will return an error since schema 1 is not supported.
341	ConvertSchema1 bool
342
343	// Platforms defines which platforms to handle when doing the image operation.
344	// Platforms is ignored when a PlatformMatcher is set, otherwise the
345	// platforms will be used to create a PlatformMatcher with no ordering
346	// preference.
347	Platforms []string
348
349	// MaxConcurrentDownloads is the max concurrent content downloads for each pull.
350	MaxConcurrentDownloads int
351
352	// AllMetadata downloads all manifests and known-configuration files
353	AllMetadata bool
354
355	// ChildLabelMap sets the labels used to reference child objects in the content
356	// store. By default, all GC reference labels will be set for all fetched content.
357	ChildLabelMap func(ocispec.Descriptor) []string
358}
359
360func defaultRemoteContext() *RemoteContext {
361	return &RemoteContext{
362		Resolver: docker.NewResolver(docker.ResolverOptions{
363			Client: http.DefaultClient,
364		}),
365	}
366}
367
368// Fetch downloads the provided content into containerd's content store
369// and returns a non-platform specific image reference
370func (c *Client) Fetch(ctx context.Context, ref string, opts ...RemoteOpt) (images.Image, error) {
371	fetchCtx := defaultRemoteContext()
372	for _, o := range opts {
373		if err := o(c, fetchCtx); err != nil {
374			return images.Image{}, err
375		}
376	}
377
378	if fetchCtx.Unpack {
379		return images.Image{}, errors.Wrap(errdefs.ErrNotImplemented, "unpack on fetch not supported, try pull")
380	}
381
382	if fetchCtx.PlatformMatcher == nil {
383		if len(fetchCtx.Platforms) == 0 {
384			fetchCtx.PlatformMatcher = platforms.All
385		} else {
386			var ps []ocispec.Platform
387			for _, s := range fetchCtx.Platforms {
388				p, err := platforms.Parse(s)
389				if err != nil {
390					return images.Image{}, errors.Wrapf(err, "invalid platform %s", s)
391				}
392				ps = append(ps, p)
393			}
394
395			fetchCtx.PlatformMatcher = platforms.Any(ps...)
396		}
397	}
398
399	ctx, done, err := c.WithLease(ctx)
400	if err != nil {
401		return images.Image{}, err
402	}
403	defer done(ctx)
404
405	img, err := c.fetch(ctx, fetchCtx, ref, 0)
406	if err != nil {
407		return images.Image{}, err
408	}
409	return c.createNewImage(ctx, img)
410}
411
412// Push uploads the provided content to a remote resource
413func (c *Client) Push(ctx context.Context, ref string, desc ocispec.Descriptor, opts ...RemoteOpt) error {
414	pushCtx := defaultRemoteContext()
415	for _, o := range opts {
416		if err := o(c, pushCtx); err != nil {
417			return err
418		}
419	}
420	if pushCtx.PlatformMatcher == nil {
421		if len(pushCtx.Platforms) > 0 {
422			var ps []ocispec.Platform
423			for _, platform := range pushCtx.Platforms {
424				p, err := platforms.Parse(platform)
425				if err != nil {
426					return errors.Wrapf(err, "invalid platform %s", platform)
427				}
428				ps = append(ps, p)
429			}
430			pushCtx.PlatformMatcher = platforms.Any(ps...)
431		} else {
432			pushCtx.PlatformMatcher = platforms.All
433		}
434	}
435
436	// Annotate ref with digest to push only push tag for single digest
437	if !strings.Contains(ref, "@") {
438		ref = ref + "@" + desc.Digest.String()
439	}
440
441	pusher, err := pushCtx.Resolver.Pusher(ctx, ref)
442	if err != nil {
443		return err
444	}
445
446	var wrapper func(images.Handler) images.Handler
447
448	if len(pushCtx.BaseHandlers) > 0 {
449		wrapper = func(h images.Handler) images.Handler {
450			h = images.Handlers(append(pushCtx.BaseHandlers, h)...)
451			if pushCtx.HandlerWrapper != nil {
452				h = pushCtx.HandlerWrapper(h)
453			}
454			return h
455		}
456	} else if pushCtx.HandlerWrapper != nil {
457		wrapper = pushCtx.HandlerWrapper
458	}
459
460	return remotes.PushContent(ctx, pusher, desc, c.ContentStore(), pushCtx.PlatformMatcher, wrapper)
461}
462
463// GetImage returns an existing image
464func (c *Client) GetImage(ctx context.Context, ref string) (Image, error) {
465	i, err := c.ImageService().Get(ctx, ref)
466	if err != nil {
467		return nil, err
468	}
469	return NewImage(c, i), nil
470}
471
472// ListImages returns all existing images
473func (c *Client) ListImages(ctx context.Context, filters ...string) ([]Image, error) {
474	imgs, err := c.ImageService().List(ctx, filters...)
475	if err != nil {
476		return nil, err
477	}
478	images := make([]Image, len(imgs))
479	for i, img := range imgs {
480		images[i] = NewImage(c, img)
481	}
482	return images, nil
483}
484
485// Restore restores a container from a checkpoint
486func (c *Client) Restore(ctx context.Context, id string, checkpoint Image, opts ...RestoreOpts) (Container, error) {
487	store := c.ContentStore()
488	index, err := decodeIndex(ctx, store, checkpoint.Target())
489	if err != nil {
490		return nil, err
491	}
492
493	ctx, done, err := c.WithLease(ctx)
494	if err != nil {
495		return nil, err
496	}
497	defer done(ctx)
498
499	copts := []NewContainerOpts{}
500	for _, o := range opts {
501		copts = append(copts, o(ctx, id, c, checkpoint, index))
502	}
503
504	ctr, err := c.NewContainer(ctx, id, copts...)
505	if err != nil {
506		return nil, err
507	}
508
509	return ctr, nil
510}
511
512func writeIndex(ctx context.Context, index *ocispec.Index, client *Client, ref string) (d ocispec.Descriptor, err error) {
513	labels := map[string]string{}
514	for i, m := range index.Manifests {
515		labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i)] = m.Digest.String()
516	}
517	data, err := json.Marshal(index)
518	if err != nil {
519		return ocispec.Descriptor{}, err
520	}
521	return writeContent(ctx, client.ContentStore(), ocispec.MediaTypeImageIndex, ref, bytes.NewReader(data), content.WithLabels(labels))
522}
523
524// GetLabel gets a label value from namespace store
525// If there is no default label, an empty string returned with nil error
526func (c *Client) GetLabel(ctx context.Context, label string) (string, error) {
527	ns, err := namespaces.NamespaceRequired(ctx)
528	if err != nil {
529		if c.defaultns == "" {
530			return "", err
531		}
532		ns = c.defaultns
533	}
534
535	srv := c.NamespaceService()
536	labels, err := srv.Labels(ctx, ns)
537	if err != nil {
538		return "", err
539	}
540
541	value := labels[label]
542	return value, nil
543}
544
545// Subscribe to events that match one or more of the provided filters.
546//
547// Callers should listen on both the envelope and errs channels. If the errs
548// channel returns nil or an error, the subscriber should terminate.
549//
550// The subscriber can stop receiving events by canceling the provided context.
551// The errs channel will be closed and return a nil error.
552func (c *Client) Subscribe(ctx context.Context, filters ...string) (ch <-chan *events.Envelope, errs <-chan error) {
553	return c.EventService().Subscribe(ctx, filters...)
554}
555
556// Close closes the clients connection to containerd
557func (c *Client) Close() error {
558	c.connMu.Lock()
559	defer c.connMu.Unlock()
560	if c.conn != nil {
561		return c.conn.Close()
562	}
563	return nil
564}
565
566// NamespaceService returns the underlying Namespaces Store
567func (c *Client) NamespaceService() namespaces.Store {
568	if c.namespaceStore != nil {
569		return c.namespaceStore
570	}
571	c.connMu.Lock()
572	defer c.connMu.Unlock()
573	return NewNamespaceStoreFromClient(namespacesapi.NewNamespacesClient(c.conn))
574}
575
576// ContainerService returns the underlying container Store
577func (c *Client) ContainerService() containers.Store {
578	if c.containerStore != nil {
579		return c.containerStore
580	}
581	c.connMu.Lock()
582	defer c.connMu.Unlock()
583	return NewRemoteContainerStore(containersapi.NewContainersClient(c.conn))
584}
585
586// ContentStore returns the underlying content Store
587func (c *Client) ContentStore() content.Store {
588	if c.contentStore != nil {
589		return c.contentStore
590	}
591	c.connMu.Lock()
592	defer c.connMu.Unlock()
593	return contentproxy.NewContentStore(contentapi.NewContentClient(c.conn))
594}
595
596// SnapshotService returns the underlying snapshotter for the provided snapshotter name
597func (c *Client) SnapshotService(snapshotterName string) snapshots.Snapshotter {
598	snapshotterName, err := c.resolveSnapshotterName(context.Background(), snapshotterName)
599	if err != nil {
600		snapshotterName = DefaultSnapshotter
601	}
602	if c.snapshotters != nil {
603		return c.snapshotters[snapshotterName]
604	}
605	c.connMu.Lock()
606	defer c.connMu.Unlock()
607	return snproxy.NewSnapshotter(snapshotsapi.NewSnapshotsClient(c.conn), snapshotterName)
608}
609
610// TaskService returns the underlying TasksClient
611func (c *Client) TaskService() tasks.TasksClient {
612	if c.taskService != nil {
613		return c.taskService
614	}
615	c.connMu.Lock()
616	defer c.connMu.Unlock()
617	return tasks.NewTasksClient(c.conn)
618}
619
620// ImageService returns the underlying image Store
621func (c *Client) ImageService() images.Store {
622	if c.imageStore != nil {
623		return c.imageStore
624	}
625	c.connMu.Lock()
626	defer c.connMu.Unlock()
627	return NewImageStoreFromClient(imagesapi.NewImagesClient(c.conn))
628}
629
630// DiffService returns the underlying Differ
631func (c *Client) DiffService() DiffService {
632	if c.diffService != nil {
633		return c.diffService
634	}
635	c.connMu.Lock()
636	defer c.connMu.Unlock()
637	return NewDiffServiceFromClient(diffapi.NewDiffClient(c.conn))
638}
639
640// IntrospectionService returns the underlying Introspection Client
641func (c *Client) IntrospectionService() introspection.Service {
642	if c.introspectionService != nil {
643		return c.introspectionService
644	}
645	c.connMu.Lock()
646	defer c.connMu.Unlock()
647	return introspection.NewIntrospectionServiceFromClient(introspectionapi.NewIntrospectionClient(c.conn))
648}
649
650// LeasesService returns the underlying Leases Client
651func (c *Client) LeasesService() leases.Manager {
652	if c.leasesService != nil {
653		return c.leasesService
654	}
655	c.connMu.Lock()
656	defer c.connMu.Unlock()
657	return leasesproxy.NewLeaseManager(leasesapi.NewLeasesClient(c.conn))
658}
659
660// HealthService returns the underlying GRPC HealthClient
661func (c *Client) HealthService() grpc_health_v1.HealthClient {
662	c.connMu.Lock()
663	defer c.connMu.Unlock()
664	return grpc_health_v1.NewHealthClient(c.conn)
665}
666
667// EventService returns the underlying event service
668func (c *Client) EventService() EventService {
669	if c.eventService != nil {
670		return c.eventService
671	}
672	c.connMu.Lock()
673	defer c.connMu.Unlock()
674	return NewEventServiceFromClient(eventsapi.NewEventsClient(c.conn))
675}
676
677// VersionService returns the underlying VersionClient
678func (c *Client) VersionService() versionservice.VersionClient {
679	c.connMu.Lock()
680	defer c.connMu.Unlock()
681	return versionservice.NewVersionClient(c.conn)
682}
683
684// Conn returns the underlying GRPC connection object
685func (c *Client) Conn() *grpc.ClientConn {
686	c.connMu.Lock()
687	defer c.connMu.Unlock()
688	return c.conn
689}
690
691// Version of containerd
692type Version struct {
693	// Version number
694	Version string
695	// Revision from git that was built
696	Revision string
697}
698
699// Version returns the version of containerd that the client is connected to
700func (c *Client) Version(ctx context.Context) (Version, error) {
701	c.connMu.Lock()
702	if c.conn == nil {
703		c.connMu.Unlock()
704		return Version{}, errors.Wrap(errdefs.ErrUnavailable, "no grpc connection available")
705	}
706	c.connMu.Unlock()
707	response, err := c.VersionService().Version(ctx, &ptypes.Empty{})
708	if err != nil {
709		return Version{}, err
710	}
711	return Version{
712		Version:  response.Version,
713		Revision: response.Revision,
714	}, nil
715}
716
717type ServerInfo struct {
718	UUID string
719}
720
721func (c *Client) Server(ctx context.Context) (ServerInfo, error) {
722	c.connMu.Lock()
723	if c.conn == nil {
724		c.connMu.Unlock()
725		return ServerInfo{}, errors.Wrap(errdefs.ErrUnavailable, "no grpc connection available")
726	}
727	c.connMu.Unlock()
728
729	response, err := c.IntrospectionService().Server(ctx, &ptypes.Empty{})
730	if err != nil {
731		return ServerInfo{}, err
732	}
733	return ServerInfo{
734		UUID: response.UUID,
735	}, nil
736}
737
738func (c *Client) resolveSnapshotterName(ctx context.Context, name string) (string, error) {
739	if name == "" {
740		label, err := c.GetLabel(ctx, defaults.DefaultSnapshotterNSLabel)
741		if err != nil {
742			return "", err
743		}
744
745		if label != "" {
746			name = label
747		} else {
748			name = DefaultSnapshotter
749		}
750	}
751
752	return name, nil
753}
754
755func (c *Client) getSnapshotter(ctx context.Context, name string) (snapshots.Snapshotter, error) {
756	name, err := c.resolveSnapshotterName(ctx, name)
757	if err != nil {
758		return nil, err
759	}
760
761	s := c.SnapshotService(name)
762	if s == nil {
763		return nil, errors.Wrapf(errdefs.ErrNotFound, "snapshotter %s was not found", name)
764	}
765
766	return s, nil
767}
768
769// CheckRuntime returns true if the current runtime matches the expected
770// runtime. Providing various parts of the runtime schema will match those
771// parts of the expected runtime
772func CheckRuntime(current, expected string) bool {
773	cp := strings.Split(current, ".")
774	l := len(cp)
775	for i, p := range strings.Split(expected, ".") {
776		if i > l {
777			return false
778		}
779		if p != cp[i] {
780			return false
781		}
782	}
783	return true
784}
785