874
client/client.go
Normal file
874
client/client.go
Normal file
@@ -0,0 +1,874 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
containersapi "github.com/containerd/containerd/v2/api/services/containers/v1"
|
||||
contentapi "github.com/containerd/containerd/v2/api/services/content/v1"
|
||||
diffapi "github.com/containerd/containerd/v2/api/services/diff/v1"
|
||||
eventsapi "github.com/containerd/containerd/v2/api/services/events/v1"
|
||||
imagesapi "github.com/containerd/containerd/v2/api/services/images/v1"
|
||||
introspectionapi "github.com/containerd/containerd/v2/api/services/introspection/v1"
|
||||
leasesapi "github.com/containerd/containerd/v2/api/services/leases/v1"
|
||||
namespacesapi "github.com/containerd/containerd/v2/api/services/namespaces/v1"
|
||||
sandboxsapi "github.com/containerd/containerd/v2/api/services/sandbox/v1"
|
||||
snapshotsapi "github.com/containerd/containerd/v2/api/services/snapshots/v1"
|
||||
"github.com/containerd/containerd/v2/api/services/tasks/v1"
|
||||
versionservice "github.com/containerd/containerd/v2/api/services/version/v1"
|
||||
apitypes "github.com/containerd/containerd/v2/api/types"
|
||||
"github.com/containerd/containerd/v2/containers"
|
||||
"github.com/containerd/containerd/v2/content"
|
||||
contentproxy "github.com/containerd/containerd/v2/content/proxy"
|
||||
"github.com/containerd/containerd/v2/defaults"
|
||||
"github.com/containerd/containerd/v2/errdefs"
|
||||
"github.com/containerd/containerd/v2/events"
|
||||
"github.com/containerd/containerd/v2/images"
|
||||
"github.com/containerd/containerd/v2/leases"
|
||||
leasesproxy "github.com/containerd/containerd/v2/leases/proxy"
|
||||
"github.com/containerd/containerd/v2/namespaces"
|
||||
"github.com/containerd/containerd/v2/pkg/dialer"
|
||||
"github.com/containerd/containerd/v2/platforms"
|
||||
"github.com/containerd/containerd/v2/plugins"
|
||||
ptypes "github.com/containerd/containerd/v2/protobuf/types"
|
||||
"github.com/containerd/containerd/v2/remotes"
|
||||
"github.com/containerd/containerd/v2/remotes/docker"
|
||||
"github.com/containerd/containerd/v2/sandbox"
|
||||
sandboxproxy "github.com/containerd/containerd/v2/sandbox/proxy"
|
||||
"github.com/containerd/containerd/v2/services/introspection"
|
||||
"github.com/containerd/containerd/v2/snapshots"
|
||||
snproxy "github.com/containerd/containerd/v2/snapshots/proxy"
|
||||
"github.com/containerd/typeurl/v2"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
"golang.org/x/sync/semaphore"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/backoff"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/health/grpc_health_v1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
const prefix = "types.containerd.io"
|
||||
// register TypeUrls for commonly marshaled external types
|
||||
major := strconv.Itoa(specs.VersionMajor)
|
||||
typeurl.Register(&specs.Spec{}, prefix, "opencontainers/runtime-spec", major, "Spec")
|
||||
typeurl.Register(&specs.Process{}, prefix, "opencontainers/runtime-spec", major, "Process")
|
||||
typeurl.Register(&specs.LinuxResources{}, prefix, "opencontainers/runtime-spec", major, "LinuxResources")
|
||||
typeurl.Register(&specs.WindowsResources{}, prefix, "opencontainers/runtime-spec", major, "WindowsResources")
|
||||
}
|
||||
|
||||
// New returns a new containerd client that is connected to the containerd
|
||||
// instance provided by address
|
||||
func New(address string, opts ...ClientOpt) (*Client, error) {
|
||||
var copts clientOpts
|
||||
for _, o := range opts {
|
||||
if err := o(&copts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if copts.timeout == 0 {
|
||||
copts.timeout = 10 * time.Second
|
||||
}
|
||||
|
||||
c := &Client{
|
||||
defaultns: copts.defaultns,
|
||||
}
|
||||
|
||||
if copts.defaultRuntime != "" {
|
||||
c.runtime = copts.defaultRuntime
|
||||
} else {
|
||||
c.runtime = defaults.DefaultRuntime
|
||||
}
|
||||
|
||||
if copts.defaultPlatform != nil {
|
||||
c.platform = copts.defaultPlatform
|
||||
} else {
|
||||
c.platform = platforms.Default()
|
||||
}
|
||||
|
||||
if copts.services != nil {
|
||||
c.services = *copts.services
|
||||
}
|
||||
if address != "" {
|
||||
backoffConfig := backoff.DefaultConfig
|
||||
backoffConfig.MaxDelay = 3 * time.Second
|
||||
connParams := grpc.ConnectParams{
|
||||
Backoff: backoffConfig,
|
||||
}
|
||||
gopts := []grpc.DialOption{
|
||||
grpc.WithBlock(),
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.FailOnNonTempDialError(true),
|
||||
grpc.WithConnectParams(connParams),
|
||||
grpc.WithContextDialer(dialer.ContextDialer),
|
||||
grpc.WithReturnConnectionError(),
|
||||
}
|
||||
if len(copts.dialOptions) > 0 {
|
||||
gopts = copts.dialOptions
|
||||
}
|
||||
gopts = append(gopts, grpc.WithDefaultCallOptions(
|
||||
grpc.MaxCallRecvMsgSize(defaults.DefaultMaxRecvMsgSize),
|
||||
grpc.MaxCallSendMsgSize(defaults.DefaultMaxSendMsgSize)))
|
||||
if len(copts.callOptions) > 0 {
|
||||
gopts = append(gopts, grpc.WithDefaultCallOptions(copts.callOptions...))
|
||||
}
|
||||
if copts.defaultns != "" {
|
||||
unary, stream := newNSInterceptors(copts.defaultns)
|
||||
gopts = append(gopts, grpc.WithChainUnaryInterceptor(unary))
|
||||
gopts = append(gopts, grpc.WithChainStreamInterceptor(stream))
|
||||
}
|
||||
|
||||
connector := func() (*grpc.ClientConn, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), copts.timeout)
|
||||
defer cancel()
|
||||
conn, err := grpc.DialContext(ctx, dialer.DialAddress(address), gopts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to dial %q: %w", address, err)
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
conn, err := connector()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.conn, c.connector = conn, connector
|
||||
}
|
||||
if copts.services == nil && c.conn == nil {
|
||||
return nil, fmt.Errorf("no grpc connection or services is available: %w", errdefs.ErrUnavailable)
|
||||
}
|
||||
|
||||
// check namespace labels for default runtime
|
||||
if copts.defaultRuntime == "" && c.defaultns != "" {
|
||||
if label, err := c.GetLabel(context.Background(), defaults.DefaultRuntimeNSLabel); err != nil {
|
||||
return nil, err
|
||||
} else if label != "" {
|
||||
c.runtime = label
|
||||
}
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// NewWithConn returns a new containerd client that is connected to the containerd
|
||||
// instance provided by the connection
|
||||
func NewWithConn(conn *grpc.ClientConn, opts ...ClientOpt) (*Client, error) {
|
||||
var copts clientOpts
|
||||
for _, o := range opts {
|
||||
if err := o(&copts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
c := &Client{
|
||||
defaultns: copts.defaultns,
|
||||
conn: conn,
|
||||
runtime: defaults.DefaultRuntime,
|
||||
}
|
||||
|
||||
if copts.defaultPlatform != nil {
|
||||
c.platform = copts.defaultPlatform
|
||||
} else {
|
||||
c.platform = platforms.Default()
|
||||
}
|
||||
|
||||
// check namespace labels for default runtime
|
||||
if copts.defaultRuntime == "" && c.defaultns != "" {
|
||||
if label, err := c.GetLabel(context.Background(), defaults.DefaultRuntimeNSLabel); err != nil {
|
||||
return nil, err
|
||||
} else if label != "" {
|
||||
c.runtime = label
|
||||
}
|
||||
}
|
||||
|
||||
if copts.services != nil {
|
||||
c.services = *copts.services
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Client is the client to interact with containerd and its various services
|
||||
// using a uniform interface
|
||||
type Client struct {
|
||||
services
|
||||
connMu sync.Mutex
|
||||
conn *grpc.ClientConn
|
||||
runtime string
|
||||
defaultns string
|
||||
platform platforms.MatchComparer
|
||||
connector func() (*grpc.ClientConn, error)
|
||||
}
|
||||
|
||||
// Reconnect re-establishes the GRPC connection to the containerd daemon
|
||||
func (c *Client) Reconnect() error {
|
||||
if c.connector == nil {
|
||||
return fmt.Errorf("unable to reconnect to containerd, no connector available: %w", errdefs.ErrUnavailable)
|
||||
}
|
||||
c.connMu.Lock()
|
||||
defer c.connMu.Unlock()
|
||||
c.conn.Close()
|
||||
conn, err := c.connector()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.conn = conn
|
||||
return nil
|
||||
}
|
||||
|
||||
// Runtime returns the name of the runtime being used
|
||||
func (c *Client) Runtime() string {
|
||||
return c.runtime
|
||||
}
|
||||
|
||||
// IsServing returns true if the client can successfully connect to the
|
||||
// containerd daemon and the healthcheck service returns the SERVING
|
||||
// response.
|
||||
// This call will block if a transient error is encountered during
|
||||
// connection. A timeout can be set in the context to ensure it returns
|
||||
// early.
|
||||
func (c *Client) IsServing(ctx context.Context) (bool, error) {
|
||||
c.connMu.Lock()
|
||||
if c.conn == nil {
|
||||
c.connMu.Unlock()
|
||||
return false, fmt.Errorf("no grpc connection available: %w", errdefs.ErrUnavailable)
|
||||
}
|
||||
c.connMu.Unlock()
|
||||
r, err := c.HealthService().Check(ctx, &grpc_health_v1.HealthCheckRequest{}, grpc.WaitForReady(true))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return r.Status == grpc_health_v1.HealthCheckResponse_SERVING, nil
|
||||
}
|
||||
|
||||
// Containers returns all containers created in containerd
|
||||
func (c *Client) Containers(ctx context.Context, filters ...string) ([]Container, error) {
|
||||
r, err := c.ContainerService().List(ctx, filters...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []Container
|
||||
for _, container := range r {
|
||||
out = append(out, containerFromRecord(c, container))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// NewContainer will create a new container with the provided id.
|
||||
// The id must be unique within the namespace.
|
||||
func (c *Client) NewContainer(ctx context.Context, id string, opts ...NewContainerOpts) (Container, error) {
|
||||
ctx, done, err := c.WithLease(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer done(ctx)
|
||||
|
||||
container := containers.Container{
|
||||
ID: id,
|
||||
Runtime: containers.RuntimeInfo{
|
||||
Name: c.runtime,
|
||||
},
|
||||
}
|
||||
for _, o := range opts {
|
||||
if err := o(ctx, c, &container); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
r, err := c.ContainerService().Create(ctx, container)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return containerFromRecord(c, r), nil
|
||||
}
|
||||
|
||||
// LoadContainer loads an existing container from metadata
|
||||
func (c *Client) LoadContainer(ctx context.Context, id string) (Container, error) {
|
||||
r, err := c.ContainerService().Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return containerFromRecord(c, r), nil
|
||||
}
|
||||
|
||||
// RemoteContext is used to configure object resolutions and transfers with
|
||||
// remote content stores and image providers.
|
||||
type RemoteContext struct {
|
||||
// Resolver is used to resolve names to objects, fetchers, and pushers.
|
||||
// If no resolver is provided, defaults to Docker registry resolver.
|
||||
Resolver remotes.Resolver
|
||||
|
||||
// PlatformMatcher is used to match the platforms for an image
|
||||
// operation and define the preference when a single match is required
|
||||
// from multiple platforms.
|
||||
PlatformMatcher platforms.MatchComparer
|
||||
|
||||
// Unpack is done after an image is pulled to extract into a snapshotter.
|
||||
// It is done simultaneously for schema 2 images when they are pulled.
|
||||
// If an image is not unpacked on pull, it can be unpacked any time
|
||||
// afterwards. Unpacking is required to run an image.
|
||||
Unpack bool
|
||||
|
||||
// UnpackOpts handles options to the unpack call.
|
||||
UnpackOpts []UnpackOpt
|
||||
|
||||
// Snapshotter used for unpacking
|
||||
Snapshotter string
|
||||
|
||||
// SnapshotterOpts are additional options to be passed to a snapshotter during pull
|
||||
SnapshotterOpts []snapshots.Opt
|
||||
|
||||
// Labels to be applied to the created image
|
||||
Labels map[string]string
|
||||
|
||||
// BaseHandlers are a set of handlers which get are called on dispatch.
|
||||
// These handlers always get called before any operation specific
|
||||
// handlers.
|
||||
BaseHandlers []images.Handler
|
||||
|
||||
// HandlerWrapper wraps the handler which gets sent to dispatch.
|
||||
// Unlike BaseHandlers, this can run before and after the built
|
||||
// in handlers, allowing operations to run on the descriptor
|
||||
// after it has completed transferring.
|
||||
HandlerWrapper func(images.Handler) images.Handler
|
||||
|
||||
// ConvertSchema1 is whether to convert Docker registry schema 1
|
||||
// manifests. If this option is false then any image which resolves
|
||||
// to schema 1 will return an error since schema 1 is not supported.
|
||||
//
|
||||
// Deprecated: use Schema 2 or OCI images.
|
||||
ConvertSchema1 bool
|
||||
|
||||
// Platforms defines which platforms to handle when doing the image operation.
|
||||
// Platforms is ignored when a PlatformMatcher is set, otherwise the
|
||||
// platforms will be used to create a PlatformMatcher with no ordering
|
||||
// preference.
|
||||
Platforms []string
|
||||
|
||||
// MaxConcurrentDownloads is the max concurrent content downloads for each pull.
|
||||
MaxConcurrentDownloads int
|
||||
|
||||
// MaxConcurrentUploadedLayers is the max concurrent uploaded layers for each push.
|
||||
MaxConcurrentUploadedLayers int
|
||||
|
||||
// AllMetadata downloads all manifests and known-configuration files
|
||||
AllMetadata bool
|
||||
|
||||
// ChildLabelMap sets the labels used to reference child objects in the content
|
||||
// store. By default, all GC reference labels will be set for all fetched content.
|
||||
ChildLabelMap func(ocispec.Descriptor) []string
|
||||
}
|
||||
|
||||
func defaultRemoteContext() *RemoteContext {
|
||||
return &RemoteContext{
|
||||
Resolver: docker.NewResolver(docker.ResolverOptions{}),
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch downloads the provided content into containerd's content store
|
||||
// and returns a non-platform specific image reference
|
||||
func (c *Client) Fetch(ctx context.Context, ref string, opts ...RemoteOpt) (images.Image, error) {
|
||||
fetchCtx := defaultRemoteContext()
|
||||
for _, o := range opts {
|
||||
if err := o(c, fetchCtx); err != nil {
|
||||
return images.Image{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if fetchCtx.Unpack {
|
||||
return images.Image{}, fmt.Errorf("unpack on fetch not supported, try pull: %w", errdefs.ErrNotImplemented)
|
||||
}
|
||||
|
||||
if fetchCtx.PlatformMatcher == nil {
|
||||
if len(fetchCtx.Platforms) == 0 {
|
||||
fetchCtx.PlatformMatcher = platforms.All
|
||||
} else {
|
||||
ps, err := platforms.ParseAll(fetchCtx.Platforms)
|
||||
if err != nil {
|
||||
return images.Image{}, err
|
||||
}
|
||||
|
||||
fetchCtx.PlatformMatcher = platforms.Any(ps...)
|
||||
}
|
||||
}
|
||||
|
||||
ctx, done, err := c.WithLease(ctx)
|
||||
if err != nil {
|
||||
return images.Image{}, err
|
||||
}
|
||||
defer done(ctx)
|
||||
|
||||
img, err := c.fetch(ctx, fetchCtx, ref, 0)
|
||||
if err != nil {
|
||||
return images.Image{}, err
|
||||
}
|
||||
return c.createNewImage(ctx, img)
|
||||
}
|
||||
|
||||
// Push uploads the provided content to a remote resource
|
||||
func (c *Client) Push(ctx context.Context, ref string, desc ocispec.Descriptor, opts ...RemoteOpt) error {
|
||||
pushCtx := defaultRemoteContext()
|
||||
for _, o := range opts {
|
||||
if err := o(c, pushCtx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if pushCtx.PlatformMatcher == nil {
|
||||
if len(pushCtx.Platforms) > 0 {
|
||||
ps, err := platforms.ParseAll(pushCtx.Platforms)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pushCtx.PlatformMatcher = platforms.Any(ps...)
|
||||
} else {
|
||||
pushCtx.PlatformMatcher = platforms.All
|
||||
}
|
||||
}
|
||||
|
||||
// Annotate ref with digest to push only push tag for single digest
|
||||
if !strings.Contains(ref, "@") {
|
||||
ref = ref + "@" + desc.Digest.String()
|
||||
}
|
||||
|
||||
pusher, err := pushCtx.Resolver.Pusher(ctx, ref)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var wrapper func(images.Handler) images.Handler
|
||||
|
||||
if len(pushCtx.BaseHandlers) > 0 {
|
||||
wrapper = func(h images.Handler) images.Handler {
|
||||
h = images.Handlers(append(pushCtx.BaseHandlers, h)...)
|
||||
if pushCtx.HandlerWrapper != nil {
|
||||
h = pushCtx.HandlerWrapper(h)
|
||||
}
|
||||
return h
|
||||
}
|
||||
} else if pushCtx.HandlerWrapper != nil {
|
||||
wrapper = pushCtx.HandlerWrapper
|
||||
}
|
||||
|
||||
var limiter *semaphore.Weighted
|
||||
if pushCtx.MaxConcurrentUploadedLayers > 0 {
|
||||
limiter = semaphore.NewWeighted(int64(pushCtx.MaxConcurrentUploadedLayers))
|
||||
}
|
||||
|
||||
return remotes.PushContent(ctx, pusher, desc, c.ContentStore(), limiter, pushCtx.PlatformMatcher, wrapper)
|
||||
}
|
||||
|
||||
// GetImage returns an existing image
|
||||
func (c *Client) GetImage(ctx context.Context, ref string) (Image, error) {
|
||||
i, err := c.ImageService().Get(ctx, ref)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewImage(c, i), nil
|
||||
}
|
||||
|
||||
// ListImages returns all existing images
|
||||
func (c *Client) ListImages(ctx context.Context, filters ...string) ([]Image, error) {
|
||||
imgs, err := c.ImageService().List(ctx, filters...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
images := make([]Image, len(imgs))
|
||||
for i, img := range imgs {
|
||||
images[i] = NewImage(c, img)
|
||||
}
|
||||
return images, nil
|
||||
}
|
||||
|
||||
// Restore restores a container from a checkpoint
|
||||
func (c *Client) Restore(ctx context.Context, id string, checkpoint Image, opts ...RestoreOpts) (Container, error) {
|
||||
store := c.ContentStore()
|
||||
index, err := decodeIndex(ctx, store, checkpoint.Target())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, done, err := c.WithLease(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer done(ctx)
|
||||
|
||||
copts := []NewContainerOpts{}
|
||||
for _, o := range opts {
|
||||
copts = append(copts, o(ctx, id, c, checkpoint, index))
|
||||
}
|
||||
|
||||
ctr, err := c.NewContainer(ctx, id, copts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ctr, nil
|
||||
}
|
||||
|
||||
func writeIndex(ctx context.Context, index *ocispec.Index, client *Client, ref string) (d ocispec.Descriptor, err error) {
|
||||
labels := map[string]string{}
|
||||
for i, m := range index.Manifests {
|
||||
labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i)] = m.Digest.String()
|
||||
}
|
||||
data, err := json.Marshal(index)
|
||||
if err != nil {
|
||||
return ocispec.Descriptor{}, err
|
||||
}
|
||||
return writeContent(ctx, client.ContentStore(), ocispec.MediaTypeImageIndex, ref, bytes.NewReader(data), content.WithLabels(labels))
|
||||
}
|
||||
|
||||
func decodeIndex(ctx context.Context, store content.Provider, desc ocispec.Descriptor) (*ocispec.Index, error) {
|
||||
var index ocispec.Index
|
||||
p, err := content.ReadBlob(ctx, store, desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(p, &index); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &index, nil
|
||||
}
|
||||
|
||||
// GetLabel gets a label value from namespace store
|
||||
// If there is no default label, an empty string returned with nil error
|
||||
func (c *Client) GetLabel(ctx context.Context, label string) (string, error) {
|
||||
ns, err := namespaces.NamespaceRequired(ctx)
|
||||
if err != nil {
|
||||
if c.defaultns == "" {
|
||||
return "", err
|
||||
}
|
||||
ns = c.defaultns
|
||||
}
|
||||
|
||||
srv := c.NamespaceService()
|
||||
labels, err := srv.Labels(ctx, ns)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
value := labels[label]
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// Subscribe to events that match one or more of the provided filters.
|
||||
//
|
||||
// Callers should listen on both the envelope and errs channels. If the errs
|
||||
// channel returns nil or an error, the subscriber should terminate.
|
||||
//
|
||||
// The subscriber can stop receiving events by canceling the provided context.
|
||||
// The errs channel will be closed and return a nil error.
|
||||
func (c *Client) Subscribe(ctx context.Context, filters ...string) (ch <-chan *events.Envelope, errs <-chan error) {
|
||||
return c.EventService().Subscribe(ctx, filters...)
|
||||
}
|
||||
|
||||
// Close closes the clients connection to containerd
|
||||
func (c *Client) Close() error {
|
||||
c.connMu.Lock()
|
||||
defer c.connMu.Unlock()
|
||||
if c.conn != nil {
|
||||
return c.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// NamespaceService returns the underlying Namespaces Store
|
||||
func (c *Client) NamespaceService() namespaces.Store {
|
||||
if c.namespaceStore != nil {
|
||||
return c.namespaceStore
|
||||
}
|
||||
c.connMu.Lock()
|
||||
defer c.connMu.Unlock()
|
||||
return NewNamespaceStoreFromClient(namespacesapi.NewNamespacesClient(c.conn))
|
||||
}
|
||||
|
||||
// ContainerService returns the underlying container Store
|
||||
func (c *Client) ContainerService() containers.Store {
|
||||
if c.containerStore != nil {
|
||||
return c.containerStore
|
||||
}
|
||||
c.connMu.Lock()
|
||||
defer c.connMu.Unlock()
|
||||
return NewRemoteContainerStore(containersapi.NewContainersClient(c.conn))
|
||||
}
|
||||
|
||||
// ContentStore returns the underlying content Store
|
||||
func (c *Client) ContentStore() content.Store {
|
||||
if c.contentStore != nil {
|
||||
return c.contentStore
|
||||
}
|
||||
c.connMu.Lock()
|
||||
defer c.connMu.Unlock()
|
||||
return contentproxy.NewContentStore(contentapi.NewContentClient(c.conn))
|
||||
}
|
||||
|
||||
// SnapshotService returns the underlying snapshotter for the provided snapshotter name
|
||||
func (c *Client) SnapshotService(snapshotterName string) snapshots.Snapshotter {
|
||||
snapshotterName, err := c.resolveSnapshotterName(context.Background(), snapshotterName)
|
||||
if err != nil {
|
||||
snapshotterName = DefaultSnapshotter
|
||||
}
|
||||
if c.snapshotters != nil {
|
||||
return c.snapshotters[snapshotterName]
|
||||
}
|
||||
c.connMu.Lock()
|
||||
defer c.connMu.Unlock()
|
||||
return snproxy.NewSnapshotter(snapshotsapi.NewSnapshotsClient(c.conn), snapshotterName)
|
||||
}
|
||||
|
||||
// DefaultNamespace return the default namespace
|
||||
func (c *Client) DefaultNamespace() string {
|
||||
return c.defaultns
|
||||
}
|
||||
|
||||
// TaskService returns the underlying TasksClient
|
||||
func (c *Client) TaskService() tasks.TasksClient {
|
||||
if c.taskService != nil {
|
||||
return c.taskService
|
||||
}
|
||||
c.connMu.Lock()
|
||||
defer c.connMu.Unlock()
|
||||
return tasks.NewTasksClient(c.conn)
|
||||
}
|
||||
|
||||
// ImageService returns the underlying image Store
|
||||
func (c *Client) ImageService() images.Store {
|
||||
if c.imageStore != nil {
|
||||
return c.imageStore
|
||||
}
|
||||
c.connMu.Lock()
|
||||
defer c.connMu.Unlock()
|
||||
return NewImageStoreFromClient(imagesapi.NewImagesClient(c.conn))
|
||||
}
|
||||
|
||||
// DiffService returns the underlying Differ
|
||||
func (c *Client) DiffService() DiffService {
|
||||
if c.diffService != nil {
|
||||
return c.diffService
|
||||
}
|
||||
c.connMu.Lock()
|
||||
defer c.connMu.Unlock()
|
||||
return NewDiffServiceFromClient(diffapi.NewDiffClient(c.conn))
|
||||
}
|
||||
|
||||
// IntrospectionService returns the underlying Introspection Client
|
||||
func (c *Client) IntrospectionService() introspection.Service {
|
||||
if c.introspectionService != nil {
|
||||
return c.introspectionService
|
||||
}
|
||||
c.connMu.Lock()
|
||||
defer c.connMu.Unlock()
|
||||
return introspection.NewIntrospectionServiceFromClient(introspectionapi.NewIntrospectionClient(c.conn))
|
||||
}
|
||||
|
||||
// LeasesService returns the underlying Leases Client
|
||||
func (c *Client) LeasesService() leases.Manager {
|
||||
if c.leasesService != nil {
|
||||
return c.leasesService
|
||||
}
|
||||
c.connMu.Lock()
|
||||
defer c.connMu.Unlock()
|
||||
return leasesproxy.NewLeaseManager(leasesapi.NewLeasesClient(c.conn))
|
||||
}
|
||||
|
||||
// HealthService returns the underlying GRPC HealthClient
|
||||
func (c *Client) HealthService() grpc_health_v1.HealthClient {
|
||||
c.connMu.Lock()
|
||||
defer c.connMu.Unlock()
|
||||
return grpc_health_v1.NewHealthClient(c.conn)
|
||||
}
|
||||
|
||||
// EventService returns the underlying event service
|
||||
func (c *Client) EventService() EventService {
|
||||
if c.eventService != nil {
|
||||
return c.eventService
|
||||
}
|
||||
c.connMu.Lock()
|
||||
defer c.connMu.Unlock()
|
||||
return NewEventServiceFromClient(eventsapi.NewEventsClient(c.conn))
|
||||
}
|
||||
|
||||
// SandboxStore returns the underlying sandbox store client
|
||||
func (c *Client) SandboxStore() sandbox.Store {
|
||||
if c.sandboxStore != nil {
|
||||
return c.sandboxStore
|
||||
}
|
||||
c.connMu.Lock()
|
||||
defer c.connMu.Unlock()
|
||||
return sandboxproxy.NewSandboxStore(sandboxsapi.NewStoreClient(c.conn))
|
||||
}
|
||||
|
||||
// SandboxController returns the underlying sandbox controller client
|
||||
func (c *Client) SandboxController(name string) sandbox.Controller {
|
||||
// default sandboxer is shim
|
||||
if c.sandboxers != nil {
|
||||
return c.sandboxers[name]
|
||||
}
|
||||
c.connMu.Lock()
|
||||
defer c.connMu.Unlock()
|
||||
return sandboxproxy.NewSandboxController(sandboxsapi.NewControllerClient(c.conn))
|
||||
}
|
||||
|
||||
// VersionService returns the underlying VersionClient
|
||||
func (c *Client) VersionService() versionservice.VersionClient {
|
||||
c.connMu.Lock()
|
||||
defer c.connMu.Unlock()
|
||||
return versionservice.NewVersionClient(c.conn)
|
||||
}
|
||||
|
||||
// Conn returns the underlying GRPC connection object
|
||||
func (c *Client) Conn() *grpc.ClientConn {
|
||||
c.connMu.Lock()
|
||||
defer c.connMu.Unlock()
|
||||
return c.conn
|
||||
}
|
||||
|
||||
// Version of containerd
|
||||
type Version struct {
|
||||
// Version number
|
||||
Version string
|
||||
// Revision from git that was built
|
||||
Revision string
|
||||
}
|
||||
|
||||
// Version returns the version of containerd that the client is connected to
|
||||
func (c *Client) Version(ctx context.Context) (Version, error) {
|
||||
c.connMu.Lock()
|
||||
if c.conn == nil {
|
||||
c.connMu.Unlock()
|
||||
return Version{}, fmt.Errorf("no grpc connection available: %w", errdefs.ErrUnavailable)
|
||||
}
|
||||
c.connMu.Unlock()
|
||||
response, err := c.VersionService().Version(ctx, &ptypes.Empty{})
|
||||
if err != nil {
|
||||
return Version{}, err
|
||||
}
|
||||
return Version{
|
||||
Version: response.Version,
|
||||
Revision: response.Revision,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ServerInfo represents the introspected server information
|
||||
type ServerInfo struct {
|
||||
UUID string
|
||||
}
|
||||
|
||||
// Server returns server information from the introspection service
|
||||
func (c *Client) Server(ctx context.Context) (ServerInfo, error) {
|
||||
c.connMu.Lock()
|
||||
if c.conn == nil {
|
||||
c.connMu.Unlock()
|
||||
return ServerInfo{}, fmt.Errorf("no grpc connection available: %w", errdefs.ErrUnavailable)
|
||||
}
|
||||
c.connMu.Unlock()
|
||||
|
||||
response, err := c.IntrospectionService().Server(ctx, &ptypes.Empty{})
|
||||
if err != nil {
|
||||
return ServerInfo{}, err
|
||||
}
|
||||
return ServerInfo{
|
||||
UUID: response.UUID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) resolveSnapshotterName(ctx context.Context, name string) (string, error) {
|
||||
if name == "" {
|
||||
label, err := c.GetLabel(ctx, defaults.DefaultSnapshotterNSLabel)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if label != "" {
|
||||
name = label
|
||||
} else {
|
||||
name = DefaultSnapshotter
|
||||
}
|
||||
}
|
||||
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func (c *Client) getSnapshotter(ctx context.Context, name string) (snapshots.Snapshotter, error) {
|
||||
name, err := c.resolveSnapshotterName(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := c.SnapshotService(name)
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("snapshotter %s was not found: %w", name, errdefs.ErrNotFound)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// GetSnapshotterSupportedPlatforms returns a platform matchers which represents the
|
||||
// supported platforms for the given snapshotters
|
||||
func (c *Client) GetSnapshotterSupportedPlatforms(ctx context.Context, snapshotterName string) (platforms.MatchComparer, error) {
|
||||
filters := []string{fmt.Sprintf("type==%s, id==%s", plugins.SnapshotPlugin, snapshotterName)}
|
||||
in := c.IntrospectionService()
|
||||
|
||||
resp, err := in.Plugins(ctx, filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(resp.Plugins) <= 0 {
|
||||
return nil, fmt.Errorf("inspection service could not find snapshotter %s plugin", snapshotterName)
|
||||
}
|
||||
|
||||
sn := resp.Plugins[0]
|
||||
snPlatforms := toPlatforms(sn.Platforms)
|
||||
return platforms.Any(snPlatforms...), nil
|
||||
}
|
||||
|
||||
func toPlatforms(pt []*apitypes.Platform) []ocispec.Platform {
|
||||
platforms := make([]ocispec.Platform, len(pt))
|
||||
for i, p := range pt {
|
||||
platforms[i] = ocispec.Platform{
|
||||
Architecture: p.Architecture,
|
||||
OS: p.OS,
|
||||
Variant: p.Variant,
|
||||
}
|
||||
}
|
||||
return platforms
|
||||
}
|
||||
|
||||
// GetSnapshotterCapabilities returns the capabilities of a snapshotter.
|
||||
func (c *Client) GetSnapshotterCapabilities(ctx context.Context, snapshotterName string) ([]string, error) {
|
||||
filters := []string{fmt.Sprintf("type==%s, id==%s", plugins.SnapshotPlugin, snapshotterName)}
|
||||
in := c.IntrospectionService()
|
||||
|
||||
resp, err := in.Plugins(ctx, filters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(resp.Plugins) <= 0 {
|
||||
return nil, fmt.Errorf("inspection service could not find snapshotter %s plugin", snapshotterName)
|
||||
}
|
||||
|
||||
sn := resp.Plugins[0]
|
||||
return sn.Capabilities, nil
|
||||
}
|
||||
256
client/client_opts.go
Normal file
256
client/client_opts.go
Normal file
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd/v2/images"
|
||||
"github.com/containerd/containerd/v2/platforms"
|
||||
"github.com/containerd/containerd/v2/remotes"
|
||||
"github.com/containerd/containerd/v2/snapshots"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type clientOpts struct {
|
||||
defaultns string
|
||||
defaultRuntime string
|
||||
defaultPlatform platforms.MatchComparer
|
||||
services *services
|
||||
dialOptions []grpc.DialOption
|
||||
callOptions []grpc.CallOption
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// ClientOpt allows callers to set options on the containerd client
|
||||
type ClientOpt func(c *clientOpts) error
|
||||
|
||||
// WithDefaultNamespace sets the default namespace on the client
|
||||
//
|
||||
// Any operation that does not have a namespace set on the context will
|
||||
// be provided the default namespace
|
||||
func WithDefaultNamespace(ns string) ClientOpt {
|
||||
return func(c *clientOpts) error {
|
||||
c.defaultns = ns
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithDefaultRuntime sets the default runtime on the client
|
||||
func WithDefaultRuntime(rt string) ClientOpt {
|
||||
return func(c *clientOpts) error {
|
||||
c.defaultRuntime = rt
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithDefaultPlatform sets the default platform matcher on the client
|
||||
func WithDefaultPlatform(platform platforms.MatchComparer) ClientOpt {
|
||||
return func(c *clientOpts) error {
|
||||
c.defaultPlatform = platform
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithDialOpts allows grpc.DialOptions to be set on the connection
|
||||
func WithDialOpts(opts []grpc.DialOption) ClientOpt {
|
||||
return func(c *clientOpts) error {
|
||||
c.dialOptions = opts
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithCallOpts allows grpc.CallOptions to be set on the connection
|
||||
func WithCallOpts(opts []grpc.CallOption) ClientOpt {
|
||||
return func(c *clientOpts) error {
|
||||
c.callOptions = opts
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithServices sets services used by the client.
|
||||
func WithServices(opts ...ServicesOpt) ClientOpt {
|
||||
return func(c *clientOpts) error {
|
||||
c.services = &services{}
|
||||
for _, o := range opts {
|
||||
o(c.services)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithTimeout sets the connection timeout for the client
|
||||
func WithTimeout(d time.Duration) ClientOpt {
|
||||
return func(c *clientOpts) error {
|
||||
c.timeout = d
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// RemoteOpt allows the caller to set distribution options for a remote
|
||||
type RemoteOpt func(*Client, *RemoteContext) error
|
||||
|
||||
// WithPlatform allows the caller to specify a platform to retrieve
|
||||
// content for
|
||||
func WithPlatform(platform string) RemoteOpt {
|
||||
if platform == "" {
|
||||
platform = platforms.DefaultString()
|
||||
}
|
||||
return func(_ *Client, c *RemoteContext) error {
|
||||
for _, p := range c.Platforms {
|
||||
if p == platform {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
c.Platforms = append(c.Platforms, platform)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithPlatformMatcher specifies the matcher to use for
|
||||
// determining which platforms to pull content for.
|
||||
// This value supersedes anything set with `WithPlatform`.
|
||||
func WithPlatformMatcher(m platforms.MatchComparer) RemoteOpt {
|
||||
return func(_ *Client, c *RemoteContext) error {
|
||||
c.PlatformMatcher = m
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithPullUnpack is used to unpack an image after pull. This
|
||||
// uses the snapshotter, content store, and diff service
|
||||
// configured for the client.
|
||||
func WithPullUnpack(_ *Client, c *RemoteContext) error {
|
||||
c.Unpack = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithUnpackOpts is used to add unpack options to the unpacker.
|
||||
func WithUnpackOpts(opts []UnpackOpt) RemoteOpt {
|
||||
return func(_ *Client, c *RemoteContext) error {
|
||||
c.UnpackOpts = append(c.UnpackOpts, opts...)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithPullSnapshotter specifies snapshotter name used for unpacking.
|
||||
func WithPullSnapshotter(snapshotterName string, opts ...snapshots.Opt) RemoteOpt {
|
||||
return func(_ *Client, c *RemoteContext) error {
|
||||
c.Snapshotter = snapshotterName
|
||||
c.SnapshotterOpts = opts
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithPullLabel sets a label to be associated with a pulled reference
|
||||
func WithPullLabel(key, value string) RemoteOpt {
|
||||
return func(_ *Client, rc *RemoteContext) error {
|
||||
if rc.Labels == nil {
|
||||
rc.Labels = make(map[string]string)
|
||||
}
|
||||
|
||||
rc.Labels[key] = value
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithPullLabels associates a set of labels to a pulled reference
|
||||
func WithPullLabels(labels map[string]string) RemoteOpt {
|
||||
return func(_ *Client, rc *RemoteContext) error {
|
||||
if rc.Labels == nil {
|
||||
rc.Labels = make(map[string]string)
|
||||
}
|
||||
|
||||
for k, v := range labels {
|
||||
rc.Labels[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithChildLabelMap sets the map function used to define the labels set
|
||||
// on referenced child content in the content store. This can be used
|
||||
// to overwrite the default GC labels or filter which labels get set
|
||||
// for content.
|
||||
// The default is `images.ChildGCLabels`.
|
||||
func WithChildLabelMap(fn func(ocispec.Descriptor) []string) RemoteOpt {
|
||||
return func(_ *Client, c *RemoteContext) error {
|
||||
c.ChildLabelMap = fn
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSchema1Conversion is used to convert Docker registry schema 1
|
||||
// manifests to oci manifests on pull. Without this option schema 1
|
||||
// manifests will return a not supported error.
|
||||
//
|
||||
// Deprecated: use Schema 2 or OCI images.
|
||||
func WithSchema1Conversion(client *Client, c *RemoteContext) error {
|
||||
c.ConvertSchema1 = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithResolver specifies the resolver to use.
|
||||
func WithResolver(resolver remotes.Resolver) RemoteOpt {
|
||||
return func(client *Client, c *RemoteContext) error {
|
||||
c.Resolver = resolver
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithImageHandler adds a base handler to be called on dispatch.
|
||||
func WithImageHandler(h images.Handler) RemoteOpt {
|
||||
return func(client *Client, c *RemoteContext) error {
|
||||
c.BaseHandlers = append(c.BaseHandlers, h)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithImageHandlerWrapper wraps the handlers to be called on dispatch.
|
||||
func WithImageHandlerWrapper(w func(images.Handler) images.Handler) RemoteOpt {
|
||||
return func(client *Client, c *RemoteContext) error {
|
||||
c.HandlerWrapper = w
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxConcurrentDownloads sets max concurrent download limit.
|
||||
func WithMaxConcurrentDownloads(max int) RemoteOpt {
|
||||
return func(client *Client, c *RemoteContext) error {
|
||||
c.MaxConcurrentDownloads = max
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxConcurrentUploadedLayers sets max concurrent uploaded layer limit.
|
||||
func WithMaxConcurrentUploadedLayers(max int) RemoteOpt {
|
||||
return func(client *Client, c *RemoteContext) error {
|
||||
c.MaxConcurrentUploadedLayers = max
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAllMetadata downloads all manifests and known-configuration files
|
||||
func WithAllMetadata() RemoteOpt {
|
||||
return func(_ *Client, c *RemoteContext) error {
|
||||
c.AllMetadata = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
466
client/container.go
Normal file
466
client/container.go
Normal file
@@ -0,0 +1,466 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/containerd/v2/api/services/tasks/v1"
|
||||
"github.com/containerd/containerd/v2/api/types"
|
||||
tasktypes "github.com/containerd/containerd/v2/api/types/task"
|
||||
"github.com/containerd/containerd/v2/cio"
|
||||
"github.com/containerd/containerd/v2/containers"
|
||||
"github.com/containerd/containerd/v2/errdefs"
|
||||
"github.com/containerd/containerd/v2/images"
|
||||
"github.com/containerd/containerd/v2/oci"
|
||||
"github.com/containerd/containerd/v2/protobuf"
|
||||
"github.com/containerd/containerd/v2/runtime/v2/runc/options"
|
||||
"github.com/containerd/fifo"
|
||||
"github.com/containerd/typeurl/v2"
|
||||
ver "github.com/opencontainers/image-spec/specs-go"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/opencontainers/selinux/go-selinux/label"
|
||||
)
|
||||
|
||||
const (
|
||||
checkpointImageNameLabel = "org.opencontainers.image.ref.name"
|
||||
checkpointRuntimeNameLabel = "io.containerd.checkpoint.runtime"
|
||||
checkpointSnapshotterNameLabel = "io.containerd.checkpoint.snapshotter"
|
||||
)
|
||||
|
||||
// Container is a metadata object for container resources and task creation
|
||||
type Container interface {
|
||||
// ID identifies the container
|
||||
ID() string
|
||||
// Info returns the underlying container record type
|
||||
Info(context.Context, ...InfoOpts) (containers.Container, error)
|
||||
// Delete removes the container
|
||||
Delete(context.Context, ...DeleteOpts) error
|
||||
// NewTask creates a new task based on the container metadata
|
||||
NewTask(context.Context, cio.Creator, ...NewTaskOpts) (Task, error)
|
||||
// Spec returns the OCI runtime specification
|
||||
Spec(context.Context) (*oci.Spec, error)
|
||||
// Task returns the current task for the container
|
||||
//
|
||||
// If cio.Load options are passed the client will Load the IO for the running
|
||||
// task.
|
||||
//
|
||||
// If cio.Attach options are passed the client will reattach to the IO for the running
|
||||
// task.
|
||||
//
|
||||
// If no task exists for the container a NotFound error is returned
|
||||
//
|
||||
// Clients must make sure that only one reader is attached to the task and consuming
|
||||
// the output from the task's fifos
|
||||
Task(context.Context, cio.Attach) (Task, error)
|
||||
// Image returns the image that the container is based on
|
||||
Image(context.Context) (Image, error)
|
||||
// Labels returns the labels set on the container
|
||||
Labels(context.Context) (map[string]string, error)
|
||||
// SetLabels sets the provided labels for the container and returns the final label set
|
||||
SetLabels(context.Context, map[string]string) (map[string]string, error)
|
||||
// Extensions returns the extensions set on the container
|
||||
Extensions(context.Context) (map[string]typeurl.Any, error)
|
||||
// Update a container
|
||||
Update(context.Context, ...UpdateContainerOpts) error
|
||||
// Checkpoint creates a checkpoint image of the current container
|
||||
Checkpoint(context.Context, string, ...CheckpointOpts) (Image, error)
|
||||
}
|
||||
|
||||
func containerFromRecord(client *Client, c containers.Container) *container {
|
||||
return &container{
|
||||
client: client,
|
||||
id: c.ID,
|
||||
metadata: c,
|
||||
}
|
||||
}
|
||||
|
||||
var _ = (Container)(&container{})
|
||||
|
||||
type container struct {
|
||||
client *Client
|
||||
id string
|
||||
metadata containers.Container
|
||||
}
|
||||
|
||||
// ID returns the container's unique id
|
||||
func (c *container) ID() string {
|
||||
return c.id
|
||||
}
|
||||
|
||||
func (c *container) Info(ctx context.Context, opts ...InfoOpts) (containers.Container, error) {
|
||||
i := &InfoConfig{
|
||||
// default to refreshing the container's local metadata
|
||||
Refresh: true,
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(i)
|
||||
}
|
||||
if i.Refresh {
|
||||
metadata, err := c.get(ctx)
|
||||
if err != nil {
|
||||
return c.metadata, err
|
||||
}
|
||||
c.metadata = metadata
|
||||
}
|
||||
return c.metadata, nil
|
||||
}
|
||||
|
||||
func (c *container) Extensions(ctx context.Context) (map[string]typeurl.Any, error) {
|
||||
r, err := c.get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.Extensions, nil
|
||||
}
|
||||
|
||||
func (c *container) Labels(ctx context.Context) (map[string]string, error) {
|
||||
r, err := c.get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.Labels, nil
|
||||
}
|
||||
|
||||
func (c *container) SetLabels(ctx context.Context, labels map[string]string) (map[string]string, error) {
|
||||
container := containers.Container{
|
||||
ID: c.id,
|
||||
Labels: labels,
|
||||
}
|
||||
|
||||
var paths []string
|
||||
// mask off paths so we only muck with the labels encountered in labels.
|
||||
// Labels not in the passed in argument will be left alone.
|
||||
for k := range labels {
|
||||
paths = append(paths, strings.Join([]string{"labels", k}, "."))
|
||||
}
|
||||
|
||||
r, err := c.client.ContainerService().Update(ctx, container, paths...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.Labels, nil
|
||||
}
|
||||
|
||||
// Spec returns the current OCI specification for the container
|
||||
func (c *container) Spec(ctx context.Context) (*oci.Spec, error) {
|
||||
r, err := c.get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var s oci.Spec
|
||||
if err := json.Unmarshal(r.Spec.GetValue(), &s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
// Delete deletes an existing container
|
||||
// an error is returned if the container has running tasks
|
||||
func (c *container) Delete(ctx context.Context, opts ...DeleteOpts) error {
|
||||
if _, err := c.loadTask(ctx, nil); err == nil {
|
||||
return fmt.Errorf("cannot delete running task %v: %w", c.id, errdefs.ErrFailedPrecondition)
|
||||
}
|
||||
r, err := c.get(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, o := range opts {
|
||||
if err := o(ctx, c.client, r); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return c.client.ContainerService().Delete(ctx, c.id)
|
||||
}
|
||||
|
||||
func (c *container) Task(ctx context.Context, attach cio.Attach) (Task, error) {
|
||||
return c.loadTask(ctx, attach)
|
||||
}
|
||||
|
||||
// Image returns the image that the container is based on
|
||||
func (c *container) Image(ctx context.Context) (Image, error) {
|
||||
r, err := c.get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.Image == "" {
|
||||
return nil, fmt.Errorf("container not created from an image: %w", errdefs.ErrNotFound)
|
||||
}
|
||||
i, err := c.client.ImageService().Get(ctx, r.Image)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get image %s for container: %w", r.Image, err)
|
||||
}
|
||||
return NewImage(c.client, i), nil
|
||||
}
|
||||
|
||||
func (c *container) NewTask(ctx context.Context, ioCreate cio.Creator, opts ...NewTaskOpts) (_ Task, err error) {
|
||||
i, err := ioCreate(c.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil && i != nil {
|
||||
i.Cancel()
|
||||
i.Close()
|
||||
}
|
||||
}()
|
||||
cfg := i.Config()
|
||||
request := &tasks.CreateTaskRequest{
|
||||
ContainerID: c.id,
|
||||
Terminal: cfg.Terminal,
|
||||
Stdin: cfg.Stdin,
|
||||
Stdout: cfg.Stdout,
|
||||
Stderr: cfg.Stderr,
|
||||
}
|
||||
r, err := c.get(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if r.SnapshotKey != "" {
|
||||
if r.Snapshotter == "" {
|
||||
return nil, fmt.Errorf("unable to resolve rootfs mounts without snapshotter on container: %w", errdefs.ErrInvalidArgument)
|
||||
}
|
||||
|
||||
// get the rootfs from the snapshotter and add it to the request
|
||||
s, err := c.client.getSnapshotter(ctx, r.Snapshotter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mounts, err := s.Mounts(ctx, r.SnapshotKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
spec, err := c.Spec(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, m := range mounts {
|
||||
if spec.Linux != nil && spec.Linux.MountLabel != "" {
|
||||
if ml := label.FormatMountLabel("", spec.Linux.MountLabel); ml != "" {
|
||||
m.Options = append(m.Options, ml)
|
||||
}
|
||||
}
|
||||
request.Rootfs = append(request.Rootfs, &types.Mount{
|
||||
Type: m.Type,
|
||||
Source: m.Source,
|
||||
Target: m.Target,
|
||||
Options: m.Options,
|
||||
})
|
||||
}
|
||||
}
|
||||
info := TaskInfo{
|
||||
runtime: r.Runtime.Name,
|
||||
}
|
||||
for _, o := range opts {
|
||||
if err := o(ctx, c.client, &info); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
for _, m := range info.RootFS {
|
||||
request.Rootfs = append(request.Rootfs, &types.Mount{
|
||||
Type: m.Type,
|
||||
Source: m.Source,
|
||||
Target: m.Target,
|
||||
Options: m.Options,
|
||||
})
|
||||
}
|
||||
request.RuntimePath = info.RuntimePath
|
||||
if info.Options != nil {
|
||||
o, err := typeurl.MarshalAny(info.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.Options = protobuf.FromAny(o)
|
||||
}
|
||||
t := &task{
|
||||
client: c.client,
|
||||
io: i,
|
||||
id: c.id,
|
||||
c: c,
|
||||
}
|
||||
if info.Checkpoint != nil {
|
||||
request.Checkpoint = info.Checkpoint
|
||||
}
|
||||
response, err := c.client.TaskService().Create(ctx, request)
|
||||
if err != nil {
|
||||
return nil, errdefs.FromGRPC(err)
|
||||
}
|
||||
t.pid = response.Pid
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (c *container) Update(ctx context.Context, opts ...UpdateContainerOpts) error {
|
||||
// fetch the current container config before updating it
|
||||
r, err := c.get(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, o := range opts {
|
||||
if err := o(ctx, c.client, &r); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := c.client.ContainerService().Update(ctx, r); err != nil {
|
||||
return errdefs.FromGRPC(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *container) Checkpoint(ctx context.Context, ref string, opts ...CheckpointOpts) (Image, error) {
|
||||
index := &ocispec.Index{
|
||||
Versioned: ver.Versioned{
|
||||
SchemaVersion: 2,
|
||||
},
|
||||
Annotations: make(map[string]string),
|
||||
}
|
||||
copts := &options.CheckpointOptions{
|
||||
Exit: false,
|
||||
OpenTcp: false,
|
||||
ExternalUnixSockets: false,
|
||||
Terminal: false,
|
||||
FileLocks: true,
|
||||
EmptyNamespaces: nil,
|
||||
}
|
||||
info, err := c.Info(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
img, err := c.Image(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, done, err := c.client.WithLease(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer done(ctx)
|
||||
|
||||
// add image name to manifest
|
||||
index.Annotations[checkpointImageNameLabel] = img.Name()
|
||||
// add runtime info to index
|
||||
index.Annotations[checkpointRuntimeNameLabel] = info.Runtime.Name
|
||||
// add snapshotter info to index
|
||||
index.Annotations[checkpointSnapshotterNameLabel] = info.Snapshotter
|
||||
|
||||
// process remaining opts
|
||||
for _, o := range opts {
|
||||
if err := o(ctx, c.client, &info, index, copts); err != nil {
|
||||
err = errdefs.FromGRPC(err)
|
||||
if !errdefs.IsAlreadyExists(err) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
desc, err := writeIndex(ctx, index, c.client, c.ID()+"index")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i := images.Image{
|
||||
Name: ref,
|
||||
Target: desc,
|
||||
}
|
||||
checkpoint, err := c.client.ImageService().Create(ctx, i)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return NewImage(c.client, checkpoint), nil
|
||||
}
|
||||
|
||||
func (c *container) loadTask(ctx context.Context, ioAttach cio.Attach) (Task, error) {
|
||||
response, err := c.client.TaskService().Get(ctx, &tasks.GetRequest{
|
||||
ContainerID: c.id,
|
||||
})
|
||||
if err != nil {
|
||||
err = errdefs.FromGRPC(err)
|
||||
if errdefs.IsNotFound(err) {
|
||||
return nil, fmt.Errorf("no running task found: %w", err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var i cio.IO
|
||||
if ioAttach != nil && response.Process.Status != tasktypes.Status_UNKNOWN {
|
||||
// Do not attach IO for task in unknown state, because there
|
||||
// are no fifo paths anyway.
|
||||
if i, err = attachExistingIO(response, ioAttach); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
t := &task{
|
||||
client: c.client,
|
||||
io: i,
|
||||
id: response.Process.ID,
|
||||
pid: response.Process.Pid,
|
||||
c: c,
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (c *container) get(ctx context.Context) (containers.Container, error) {
|
||||
return c.client.ContainerService().Get(ctx, c.id)
|
||||
}
|
||||
|
||||
// get the existing fifo paths from the task information stored by the daemon
|
||||
func attachExistingIO(response *tasks.GetResponse, ioAttach cio.Attach) (cio.IO, error) {
|
||||
fifoSet := loadFifos(response)
|
||||
return ioAttach(fifoSet)
|
||||
}
|
||||
|
||||
// loadFifos loads the containers fifos
|
||||
func loadFifos(response *tasks.GetResponse) *cio.FIFOSet {
|
||||
fifos := []string{
|
||||
response.Process.Stdin,
|
||||
response.Process.Stdout,
|
||||
response.Process.Stderr,
|
||||
}
|
||||
closer := func() error {
|
||||
var (
|
||||
err error
|
||||
dirs = map[string]struct{}{}
|
||||
)
|
||||
for _, f := range fifos {
|
||||
if isFifo, _ := fifo.IsFifo(f); isFifo {
|
||||
if rerr := os.Remove(f); err == nil {
|
||||
err = rerr
|
||||
}
|
||||
dirs[filepath.Dir(f)] = struct{}{}
|
||||
}
|
||||
}
|
||||
for dir := range dirs {
|
||||
// we ignore errors here because we don't
|
||||
// want to remove the directory if it isn't
|
||||
// empty
|
||||
_ = os.Remove(dir)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
return cio.NewFIFOSet(cio.Config{
|
||||
Stdin: response.Process.Stdin,
|
||||
Stdout: response.Process.Stdout,
|
||||
Stderr: response.Process.Stderr,
|
||||
Terminal: response.Process.Terminal,
|
||||
}, closer)
|
||||
}
|
||||
159
client/container_checkpoint_opts.go
Normal file
159
client/container_checkpoint_opts.go
Normal file
@@ -0,0 +1,159 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
tasks "github.com/containerd/containerd/v2/api/services/tasks/v1"
|
||||
"github.com/containerd/containerd/v2/containers"
|
||||
"github.com/containerd/containerd/v2/diff"
|
||||
"github.com/containerd/containerd/v2/images"
|
||||
"github.com/containerd/containerd/v2/platforms"
|
||||
"github.com/containerd/containerd/v2/protobuf"
|
||||
"github.com/containerd/containerd/v2/protobuf/proto"
|
||||
"github.com/containerd/containerd/v2/rootfs"
|
||||
"github.com/containerd/containerd/v2/runtime/v2/runc/options"
|
||||
"github.com/opencontainers/go-digest"
|
||||
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrCheckpointRWUnsupported is returned if the container runtime does not support checkpoint
|
||||
ErrCheckpointRWUnsupported = errors.New("rw checkpoint is only supported on v2 runtimes")
|
||||
// ErrMediaTypeNotFound returns an error when a media type in the manifest is unknown
|
||||
ErrMediaTypeNotFound = errors.New("media type not found")
|
||||
)
|
||||
|
||||
// CheckpointOpts are options to manage the checkpoint operation
|
||||
type CheckpointOpts func(context.Context, *Client, *containers.Container, *imagespec.Index, *options.CheckpointOptions) error
|
||||
|
||||
// WithCheckpointImage includes the container image in the checkpoint
|
||||
func WithCheckpointImage(ctx context.Context, client *Client, c *containers.Container, index *imagespec.Index, copts *options.CheckpointOptions) error {
|
||||
ir, err := client.ImageService().Get(ctx, c.Image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index.Manifests = append(index.Manifests, ir.Target)
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithCheckpointTask includes the running task
|
||||
func WithCheckpointTask(ctx context.Context, client *Client, c *containers.Container, index *imagespec.Index, copts *options.CheckpointOptions) error {
|
||||
opt, err := protobuf.MarshalAnyToProto(copts)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
task, err := client.TaskService().Checkpoint(ctx, &tasks.CheckpointTaskRequest{
|
||||
ContainerID: c.ID,
|
||||
Options: opt,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, d := range task.Descriptors {
|
||||
platformSpec := platforms.DefaultSpec()
|
||||
index.Manifests = append(index.Manifests, imagespec.Descriptor{
|
||||
MediaType: d.MediaType,
|
||||
Size: d.Size,
|
||||
Digest: digest.Digest(d.Digest),
|
||||
Platform: &platformSpec,
|
||||
Annotations: d.Annotations,
|
||||
})
|
||||
}
|
||||
// save copts
|
||||
data, err := proto.Marshal(opt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r := bytes.NewReader(data)
|
||||
desc, err := writeContent(ctx, client.ContentStore(), images.MediaTypeContainerd1CheckpointOptions, c.ID+"-checkpoint-options", r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
desc.Platform = &imagespec.Platform{
|
||||
OS: runtime.GOOS,
|
||||
Architecture: runtime.GOARCH,
|
||||
}
|
||||
index.Manifests = append(index.Manifests, desc)
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithCheckpointRuntime includes the container runtime info
|
||||
func WithCheckpointRuntime(ctx context.Context, client *Client, c *containers.Container, index *imagespec.Index, copts *options.CheckpointOptions) error {
|
||||
if c.Runtime.Options != nil && c.Runtime.Options.GetValue() != nil {
|
||||
opt := protobuf.FromAny(c.Runtime.Options)
|
||||
data, err := proto.Marshal(opt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r := bytes.NewReader(data)
|
||||
desc, err := writeContent(ctx, client.ContentStore(), images.MediaTypeContainerd1CheckpointRuntimeOptions, c.ID+"-runtime-options", r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
desc.Platform = &imagespec.Platform{
|
||||
OS: runtime.GOOS,
|
||||
Architecture: runtime.GOARCH,
|
||||
}
|
||||
index.Manifests = append(index.Manifests, desc)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithCheckpointRW includes the rw in the checkpoint
|
||||
func WithCheckpointRW(ctx context.Context, client *Client, c *containers.Container, index *imagespec.Index, copts *options.CheckpointOptions) error {
|
||||
diffOpts := []diff.Opt{
|
||||
diff.WithReference(fmt.Sprintf("checkpoint-rw-%s", c.SnapshotKey)),
|
||||
}
|
||||
rw, err := rootfs.CreateDiff(ctx,
|
||||
c.SnapshotKey,
|
||||
client.SnapshotService(c.Snapshotter),
|
||||
client.DiffService(),
|
||||
diffOpts...,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
}
|
||||
rw.Platform = &imagespec.Platform{
|
||||
OS: runtime.GOOS,
|
||||
Architecture: runtime.GOARCH,
|
||||
}
|
||||
index.Manifests = append(index.Manifests, rw)
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithCheckpointTaskExit causes the task to exit after checkpoint
|
||||
func WithCheckpointTaskExit(ctx context.Context, client *Client, c *containers.Container, index *imagespec.Index, copts *options.CheckpointOptions) error {
|
||||
copts.Exit = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIndexByMediaType returns the index in a manifest for the specified media type
|
||||
func GetIndexByMediaType(index *imagespec.Index, mt string) (*imagespec.Descriptor, error) {
|
||||
for _, d := range index.Manifests {
|
||||
if d.MediaType == mt {
|
||||
return &d, nil
|
||||
}
|
||||
}
|
||||
return nil, ErrMediaTypeNotFound
|
||||
}
|
||||
332
client/container_opts.go
Normal file
332
client/container_opts.go
Normal file
@@ -0,0 +1,332 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/containerd/containerd/v2/containers"
|
||||
"github.com/containerd/containerd/v2/content"
|
||||
"github.com/containerd/containerd/v2/errdefs"
|
||||
"github.com/containerd/containerd/v2/images"
|
||||
"github.com/containerd/containerd/v2/namespaces"
|
||||
"github.com/containerd/containerd/v2/oci"
|
||||
"github.com/containerd/containerd/v2/protobuf"
|
||||
"github.com/containerd/containerd/v2/snapshots"
|
||||
"github.com/containerd/typeurl/v2"
|
||||
"github.com/opencontainers/image-spec/identity"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// DeleteOpts allows the caller to set options for the deletion of a container
|
||||
type DeleteOpts func(ctx context.Context, client *Client, c containers.Container) error
|
||||
|
||||
// NewContainerOpts allows the caller to set additional options when creating a container
|
||||
type NewContainerOpts func(ctx context.Context, client *Client, c *containers.Container) error
|
||||
|
||||
// UpdateContainerOpts allows the caller to set additional options when updating a container
|
||||
type UpdateContainerOpts func(ctx context.Context, client *Client, c *containers.Container) error
|
||||
|
||||
// InfoOpts controls how container metadata is fetched and returned
|
||||
type InfoOpts func(*InfoConfig)
|
||||
|
||||
// InfoConfig specifies how container metadata is fetched
|
||||
type InfoConfig struct {
|
||||
// Refresh will to a fetch of the latest container metadata
|
||||
Refresh bool
|
||||
}
|
||||
|
||||
// WithRuntime allows a user to specify the runtime name and additional options that should
|
||||
// be used to create tasks for the container
|
||||
func WithRuntime(name string, options interface{}) NewContainerOpts {
|
||||
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
||||
var (
|
||||
opts typeurl.Any
|
||||
err error
|
||||
)
|
||||
if options != nil {
|
||||
opts, err = typeurl.MarshalAny(options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
c.Runtime = containers.RuntimeInfo{
|
||||
Name: name,
|
||||
Options: opts,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSandbox joins the container to a container group (aka sandbox) from the given ID
|
||||
// Note: shim runtime must support sandboxes environments.
|
||||
func WithSandbox(sandboxID string) NewContainerOpts {
|
||||
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
||||
c.SandboxID = sandboxID
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithImage sets the provided image as the base for the container
|
||||
func WithImage(i Image) NewContainerOpts {
|
||||
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
||||
c.Image = i.Name()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithImageName allows setting the image name as the base for the container
|
||||
func WithImageName(n string) NewContainerOpts {
|
||||
return func(ctx context.Context, _ *Client, c *containers.Container) error {
|
||||
c.Image = n
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithContainerLabels sets the provided labels to the container.
|
||||
// The existing labels are cleared.
|
||||
// Use WithAdditionalContainerLabels to preserve the existing labels.
|
||||
func WithContainerLabels(labels map[string]string) NewContainerOpts {
|
||||
return func(_ context.Context, _ *Client, c *containers.Container) error {
|
||||
c.Labels = labels
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithImageConfigLabels sets the image config labels on the container.
|
||||
// The existing labels are cleared as this is expected to be the first
|
||||
// operation in setting up a container's labels. Use WithAdditionalContainerLabels
|
||||
// to add/overwrite the existing image config labels.
|
||||
func WithImageConfigLabels(image Image) NewContainerOpts {
|
||||
return func(ctx context.Context, _ *Client, c *containers.Container) error {
|
||||
ic, err := image.Config(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !images.IsConfigType(ic.MediaType) {
|
||||
return fmt.Errorf("unknown image config media type %s", ic.MediaType)
|
||||
}
|
||||
|
||||
var (
|
||||
ociimage v1.Image
|
||||
config v1.ImageConfig
|
||||
)
|
||||
p, err := content.ReadBlob(ctx, image.ContentStore(), ic)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(p, &ociimage); err != nil {
|
||||
return err
|
||||
}
|
||||
config = ociimage.Config
|
||||
|
||||
c.Labels = config.Labels
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAdditionalContainerLabels adds the provided labels to the container
|
||||
// The existing labels are preserved as long as they do not conflict with the added labels.
|
||||
func WithAdditionalContainerLabels(labels map[string]string) NewContainerOpts {
|
||||
return func(_ context.Context, _ *Client, c *containers.Container) error {
|
||||
if c.Labels == nil {
|
||||
c.Labels = labels
|
||||
return nil
|
||||
}
|
||||
for k, v := range labels {
|
||||
c.Labels[k] = v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithImageStopSignal sets a well-known containerd label (StopSignalLabel)
|
||||
// on the container for storing the stop signal specified in the OCI image
|
||||
// config
|
||||
func WithImageStopSignal(image Image, defaultSignal string) NewContainerOpts {
|
||||
return func(ctx context.Context, _ *Client, c *containers.Container) error {
|
||||
if c.Labels == nil {
|
||||
c.Labels = make(map[string]string)
|
||||
}
|
||||
stopSignal, err := GetOCIStopSignal(ctx, image, defaultSignal)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Labels[StopSignalLabel] = stopSignal
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSnapshotter sets the provided snapshotter for use by the container
|
||||
//
|
||||
// This option must appear before other snapshotter options to have an effect.
|
||||
func WithSnapshotter(name string) NewContainerOpts {
|
||||
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
||||
c.Snapshotter = name
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSnapshot uses an existing root filesystem for the container
|
||||
func WithSnapshot(id string) NewContainerOpts {
|
||||
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
||||
// check that the snapshot exists, if not, fail on creation
|
||||
var err error
|
||||
c.Snapshotter, err = client.resolveSnapshotterName(ctx, c.Snapshotter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s, err := client.getSnapshotter(ctx, c.Snapshotter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := s.Mounts(ctx, id); err != nil {
|
||||
return err
|
||||
}
|
||||
c.SnapshotKey = id
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSnapshotCleanup deletes the rootfs snapshot allocated for the container
|
||||
func WithSnapshotCleanup(ctx context.Context, client *Client, c containers.Container) error {
|
||||
if c.SnapshotKey != "" {
|
||||
if c.Snapshotter == "" {
|
||||
return fmt.Errorf("container.Snapshotter must be set to cleanup rootfs snapshot: %w", errdefs.ErrInvalidArgument)
|
||||
}
|
||||
s, err := client.getSnapshotter(ctx, c.Snapshotter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.Remove(ctx, c.SnapshotKey); err != nil && !errdefs.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithNewSnapshot allocates a new snapshot to be used by the container as the
|
||||
// root filesystem in read-write mode
|
||||
func WithNewSnapshot(id string, i Image, opts ...snapshots.Opt) NewContainerOpts {
|
||||
return withNewSnapshot(id, i, false, opts...)
|
||||
}
|
||||
|
||||
// WithNewSnapshotView allocates a new snapshot to be used by the container as the
|
||||
// root filesystem in read-only mode
|
||||
func WithNewSnapshotView(id string, i Image, opts ...snapshots.Opt) NewContainerOpts {
|
||||
return withNewSnapshot(id, i, true, opts...)
|
||||
}
|
||||
|
||||
func withNewSnapshot(id string, i Image, readonly bool, opts ...snapshots.Opt) NewContainerOpts {
|
||||
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
||||
diffIDs, err := i.RootFS(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
parent := identity.ChainID(diffIDs).String()
|
||||
c.Snapshotter, err = client.resolveSnapshotterName(ctx, c.Snapshotter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s, err := client.getSnapshotter(ctx, c.Snapshotter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parent, err = resolveSnapshotOptions(ctx, client, c.Snapshotter, s, parent, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if readonly {
|
||||
_, err = s.View(ctx, id, parent, opts...)
|
||||
} else {
|
||||
_, err = s.Prepare(ctx, id, parent, opts...)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.SnapshotKey = id
|
||||
c.Image = i.Name()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithContainerExtension appends extension data to the container object.
|
||||
// Use this to decorate the container object with additional data for the client
|
||||
// integration.
|
||||
//
|
||||
// Make sure to register the type of `extension` in the typeurl package via
|
||||
// `typeurl.Register` or container creation may fail.
|
||||
func WithContainerExtension(name string, extension interface{}) NewContainerOpts {
|
||||
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("extension key must not be zero-length: %w", errdefs.ErrInvalidArgument)
|
||||
}
|
||||
|
||||
ext, err := typeurl.MarshalAny(extension)
|
||||
if err != nil {
|
||||
if errors.Is(err, typeurl.ErrNotFound) {
|
||||
return fmt.Errorf("extension %q is not registered with the typeurl package, see `typeurl.Register`: %w", name, err)
|
||||
}
|
||||
return fmt.Errorf("error marshalling extension: %w", err)
|
||||
}
|
||||
|
||||
if c.Extensions == nil {
|
||||
c.Extensions = make(map[string]typeurl.Any)
|
||||
}
|
||||
c.Extensions[name] = ext
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithNewSpec generates a new spec for a new container
|
||||
func WithNewSpec(opts ...oci.SpecOpts) NewContainerOpts {
|
||||
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
||||
if _, ok := namespaces.Namespace(ctx); !ok {
|
||||
ctx = namespaces.WithNamespace(ctx, client.DefaultNamespace())
|
||||
}
|
||||
s, err := oci.GenerateSpec(ctx, client, c, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Spec, err = typeurl.MarshalAny(s)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// WithSpec sets the provided spec on the container
|
||||
func WithSpec(s *oci.Spec, opts ...oci.SpecOpts) NewContainerOpts {
|
||||
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
||||
if err := oci.ApplyOpts(ctx, client, c, s, opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var err error
|
||||
c.Spec, err = protobuf.MarshalAnyToProto(s)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// WithoutRefreshedMetadata will use the current metadata attached to the container object
|
||||
func WithoutRefreshedMetadata(i *InfoConfig) {
|
||||
i.Refresh = false
|
||||
}
|
||||
116
client/container_opts_unix.go
Normal file
116
client/container_opts_unix.go
Normal file
@@ -0,0 +1,116 @@
|
||||
//go:build !windows
|
||||
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/containerd/containerd/v2/containers"
|
||||
"github.com/containerd/containerd/v2/errdefs"
|
||||
"github.com/containerd/containerd/v2/mount"
|
||||
"github.com/opencontainers/image-spec/identity"
|
||||
)
|
||||
|
||||
// WithRemappedSnapshot creates a new snapshot and remaps the uid/gid for the
|
||||
// filesystem to be used by a container with user namespaces
|
||||
func WithRemappedSnapshot(id string, i Image, uid, gid uint32) NewContainerOpts {
|
||||
return withRemappedSnapshotBase(id, i, uid, gid, false)
|
||||
}
|
||||
|
||||
// WithRemappedSnapshotView is similar to WithRemappedSnapshot but rootfs is mounted as read-only.
|
||||
func WithRemappedSnapshotView(id string, i Image, uid, gid uint32) NewContainerOpts {
|
||||
return withRemappedSnapshotBase(id, i, uid, gid, true)
|
||||
}
|
||||
|
||||
func withRemappedSnapshotBase(id string, i Image, uid, gid uint32, readonly bool) NewContainerOpts {
|
||||
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
||||
diffIDs, err := i.(*image).i.RootFS(ctx, client.ContentStore(), client.platform)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
parent = identity.ChainID(diffIDs).String()
|
||||
usernsID = fmt.Sprintf("%s-%d-%d", parent, uid, gid)
|
||||
)
|
||||
c.Snapshotter, err = client.resolveSnapshotterName(ctx, c.Snapshotter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
snapshotter, err := client.getSnapshotter(ctx, c.Snapshotter)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := snapshotter.Stat(ctx, usernsID); err == nil {
|
||||
if _, err := snapshotter.Prepare(ctx, id, usernsID); err == nil {
|
||||
c.SnapshotKey = id
|
||||
c.Image = i.Name()
|
||||
return nil
|
||||
} else if !errdefs.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
mounts, err := snapshotter.Prepare(ctx, usernsID+"-remap", parent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := remapRootFS(ctx, mounts, uid, gid); err != nil {
|
||||
snapshotter.Remove(ctx, usernsID)
|
||||
return err
|
||||
}
|
||||
if err := snapshotter.Commit(ctx, usernsID, usernsID+"-remap"); err != nil {
|
||||
return err
|
||||
}
|
||||
if readonly {
|
||||
_, err = snapshotter.View(ctx, id, usernsID)
|
||||
} else {
|
||||
_, err = snapshotter.Prepare(ctx, id, usernsID)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.SnapshotKey = id
|
||||
c.Image = i.Name()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func remapRootFS(ctx context.Context, mounts []mount.Mount, uid, gid uint32) error {
|
||||
return mount.WithTempMount(ctx, mounts, func(root string) error {
|
||||
return filepath.Walk(root, incrementFS(root, uid, gid))
|
||||
})
|
||||
}
|
||||
|
||||
func incrementFS(root string, uidInc, gidInc uint32) filepath.WalkFunc {
|
||||
return func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
stat = info.Sys().(*syscall.Stat_t)
|
||||
u, g = int(stat.Uid + uidInc), int(stat.Gid + gidInc)
|
||||
)
|
||||
// be sure the lchown the path as to not de-reference the symlink to a host file
|
||||
return os.Lchown(path, u, g)
|
||||
}
|
||||
}
|
||||
150
client/container_restore_opts.go
Normal file
150
client/container_restore_opts.go
Normal file
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/containerd/containerd/v2/containers"
|
||||
"github.com/containerd/containerd/v2/content"
|
||||
"github.com/containerd/containerd/v2/images"
|
||||
"github.com/containerd/containerd/v2/protobuf/proto"
|
||||
ptypes "github.com/containerd/containerd/v2/protobuf/types"
|
||||
"github.com/opencontainers/image-spec/identity"
|
||||
imagespec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrImageNameNotFoundInIndex is returned when the image name is not found in the index
|
||||
ErrImageNameNotFoundInIndex = errors.New("image name not found in index")
|
||||
// ErrRuntimeNameNotFoundInIndex is returned when the runtime is not found in the index
|
||||
ErrRuntimeNameNotFoundInIndex = errors.New("runtime not found in index")
|
||||
// ErrSnapshotterNameNotFoundInIndex is returned when the snapshotter is not found in the index
|
||||
ErrSnapshotterNameNotFoundInIndex = errors.New("snapshotter not found in index")
|
||||
)
|
||||
|
||||
// RestoreOpts are options to manage the restore operation
|
||||
type RestoreOpts func(context.Context, string, *Client, Image, *imagespec.Index) NewContainerOpts
|
||||
|
||||
// WithRestoreImage restores the image for the container
|
||||
func WithRestoreImage(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) NewContainerOpts {
|
||||
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
||||
name, ok := index.Annotations[checkpointImageNameLabel]
|
||||
if !ok || name == "" {
|
||||
return ErrImageNameNotFoundInIndex
|
||||
}
|
||||
snapshotter, ok := index.Annotations[checkpointSnapshotterNameLabel]
|
||||
if !ok || name == "" {
|
||||
return ErrSnapshotterNameNotFoundInIndex
|
||||
}
|
||||
i, err := client.GetImage(ctx, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
diffIDs, err := i.(*image).i.RootFS(ctx, client.ContentStore(), client.platform)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parent := identity.ChainID(diffIDs).String()
|
||||
if _, err := client.SnapshotService(snapshotter).Prepare(ctx, id, parent); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Image = i.Name()
|
||||
c.SnapshotKey = id
|
||||
c.Snapshotter = snapshotter
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithRestoreRuntime restores the runtime for the container
|
||||
func WithRestoreRuntime(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) NewContainerOpts {
|
||||
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
||||
name, ok := index.Annotations[checkpointRuntimeNameLabel]
|
||||
if !ok {
|
||||
return ErrRuntimeNameNotFoundInIndex
|
||||
}
|
||||
|
||||
// restore options if present
|
||||
m, err := GetIndexByMediaType(index, images.MediaTypeContainerd1CheckpointRuntimeOptions)
|
||||
if err != nil {
|
||||
if err != ErrMediaTypeNotFound {
|
||||
return err
|
||||
}
|
||||
}
|
||||
var options ptypes.Any
|
||||
if m != nil {
|
||||
store := client.ContentStore()
|
||||
data, err := content.ReadBlob(ctx, store, *m)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read checkpoint runtime: %w", err)
|
||||
}
|
||||
if err := proto.Unmarshal(data, &options); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.Runtime = containers.RuntimeInfo{
|
||||
Name: name,
|
||||
Options: &options,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithRestoreSpec restores the spec from the checkpoint for the container
|
||||
func WithRestoreSpec(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) NewContainerOpts {
|
||||
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
||||
m, err := GetIndexByMediaType(index, images.MediaTypeContainerd1CheckpointConfig)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store := client.ContentStore()
|
||||
data, err := content.ReadBlob(ctx, store, *m)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read checkpoint config: %w", err)
|
||||
}
|
||||
var any ptypes.Any
|
||||
if err := proto.Unmarshal(data, &any); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Spec = &any
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithRestoreRW restores the rw layer from the checkpoint for the container
|
||||
func WithRestoreRW(ctx context.Context, id string, client *Client, checkpoint Image, index *imagespec.Index) NewContainerOpts {
|
||||
return func(ctx context.Context, client *Client, c *containers.Container) error {
|
||||
// apply rw layer
|
||||
rw, err := GetIndexByMediaType(index, imagespec.MediaTypeImageLayerGzip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mounts, err := client.SnapshotService(c.Snapshotter).Mounts(ctx, c.SnapshotKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := client.DiffService().Apply(ctx, *rw, mounts); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
210
client/containerstore.go
Normal file
210
client/containerstore.go
Normal file
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
containersapi "github.com/containerd/containerd/v2/api/services/containers/v1"
|
||||
"github.com/containerd/containerd/v2/containers"
|
||||
"github.com/containerd/containerd/v2/errdefs"
|
||||
"github.com/containerd/containerd/v2/protobuf"
|
||||
ptypes "github.com/containerd/containerd/v2/protobuf/types"
|
||||
"github.com/containerd/typeurl/v2"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
type remoteContainers struct {
|
||||
client containersapi.ContainersClient
|
||||
}
|
||||
|
||||
var _ containers.Store = &remoteContainers{}
|
||||
|
||||
// NewRemoteContainerStore returns the container Store connected with the provided client
|
||||
func NewRemoteContainerStore(client containersapi.ContainersClient) containers.Store {
|
||||
return &remoteContainers{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *remoteContainers) Get(ctx context.Context, id string) (containers.Container, error) {
|
||||
resp, err := r.client.Get(ctx, &containersapi.GetContainerRequest{
|
||||
ID: id,
|
||||
})
|
||||
if err != nil {
|
||||
return containers.Container{}, errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
return containerFromProto(resp.Container), nil
|
||||
}
|
||||
|
||||
func (r *remoteContainers) List(ctx context.Context, filters ...string) ([]containers.Container, error) {
|
||||
containers, err := r.stream(ctx, filters...)
|
||||
if err != nil {
|
||||
if err == errStreamNotAvailable {
|
||||
return r.list(ctx, filters...)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return containers, nil
|
||||
}
|
||||
|
||||
func (r *remoteContainers) list(ctx context.Context, filters ...string) ([]containers.Container, error) {
|
||||
resp, err := r.client.List(ctx, &containersapi.ListContainersRequest{
|
||||
Filters: filters,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errdefs.FromGRPC(err)
|
||||
}
|
||||
return containersFromProto(resp.Containers), nil
|
||||
}
|
||||
|
||||
var errStreamNotAvailable = errors.New("streaming api not available")
|
||||
|
||||
func (r *remoteContainers) stream(ctx context.Context, filters ...string) ([]containers.Container, error) {
|
||||
session, err := r.client.ListStream(ctx, &containersapi.ListContainersRequest{
|
||||
Filters: filters,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errdefs.FromGRPC(err)
|
||||
}
|
||||
var containers []containers.Container
|
||||
for {
|
||||
c, err := session.Recv()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
return containers, nil
|
||||
}
|
||||
if s, ok := status.FromError(err); ok {
|
||||
if s.Code() == codes.Unimplemented {
|
||||
return nil, errStreamNotAvailable
|
||||
}
|
||||
}
|
||||
return nil, errdefs.FromGRPC(err)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return containers, ctx.Err()
|
||||
default:
|
||||
containers = append(containers, containerFromProto(c.Container))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *remoteContainers) Create(ctx context.Context, container containers.Container) (containers.Container, error) {
|
||||
created, err := r.client.Create(ctx, &containersapi.CreateContainerRequest{
|
||||
Container: containerToProto(&container),
|
||||
})
|
||||
if err != nil {
|
||||
return containers.Container{}, errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
return containerFromProto(created.Container), nil
|
||||
|
||||
}
|
||||
|
||||
func (r *remoteContainers) Update(ctx context.Context, container containers.Container, fieldpaths ...string) (containers.Container, error) {
|
||||
var updateMask *ptypes.FieldMask
|
||||
if len(fieldpaths) > 0 {
|
||||
updateMask = &ptypes.FieldMask{
|
||||
Paths: fieldpaths,
|
||||
}
|
||||
}
|
||||
|
||||
updated, err := r.client.Update(ctx, &containersapi.UpdateContainerRequest{
|
||||
Container: containerToProto(&container),
|
||||
UpdateMask: updateMask,
|
||||
})
|
||||
if err != nil {
|
||||
return containers.Container{}, errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
return containerFromProto(updated.Container), nil
|
||||
|
||||
}
|
||||
|
||||
func (r *remoteContainers) Delete(ctx context.Context, id string) error {
|
||||
_, err := r.client.Delete(ctx, &containersapi.DeleteContainerRequest{
|
||||
ID: id,
|
||||
})
|
||||
|
||||
return errdefs.FromGRPC(err)
|
||||
|
||||
}
|
||||
|
||||
func containerToProto(container *containers.Container) *containersapi.Container {
|
||||
extensions := make(map[string]*ptypes.Any)
|
||||
for k, v := range container.Extensions {
|
||||
extensions[k] = protobuf.FromAny(v)
|
||||
}
|
||||
return &containersapi.Container{
|
||||
ID: container.ID,
|
||||
Labels: container.Labels,
|
||||
Image: container.Image,
|
||||
Runtime: &containersapi.Container_Runtime{
|
||||
Name: container.Runtime.Name,
|
||||
Options: protobuf.FromAny(container.Runtime.Options),
|
||||
},
|
||||
Spec: protobuf.FromAny(container.Spec),
|
||||
Snapshotter: container.Snapshotter,
|
||||
SnapshotKey: container.SnapshotKey,
|
||||
Extensions: extensions,
|
||||
Sandbox: container.SandboxID,
|
||||
}
|
||||
}
|
||||
|
||||
func containerFromProto(containerpb *containersapi.Container) containers.Container {
|
||||
var runtime containers.RuntimeInfo
|
||||
if containerpb.Runtime != nil {
|
||||
runtime = containers.RuntimeInfo{
|
||||
Name: containerpb.Runtime.Name,
|
||||
Options: containerpb.Runtime.Options,
|
||||
}
|
||||
}
|
||||
extensions := make(map[string]typeurl.Any)
|
||||
for k, v := range containerpb.Extensions {
|
||||
v := v
|
||||
extensions[k] = v
|
||||
}
|
||||
return containers.Container{
|
||||
ID: containerpb.ID,
|
||||
Labels: containerpb.Labels,
|
||||
Image: containerpb.Image,
|
||||
Runtime: runtime,
|
||||
Spec: containerpb.Spec,
|
||||
Snapshotter: containerpb.Snapshotter,
|
||||
SnapshotKey: containerpb.SnapshotKey,
|
||||
CreatedAt: protobuf.FromTimestamp(containerpb.CreatedAt),
|
||||
UpdatedAt: protobuf.FromTimestamp(containerpb.UpdatedAt),
|
||||
Extensions: extensions,
|
||||
SandboxID: containerpb.Sandbox,
|
||||
}
|
||||
}
|
||||
|
||||
func containersFromProto(containerspb []*containersapi.Container) []containers.Container {
|
||||
var containers []containers.Container
|
||||
|
||||
for _, container := range containerspb {
|
||||
container := container
|
||||
containers = append(containers, containerFromProto(container))
|
||||
}
|
||||
|
||||
return containers
|
||||
}
|
||||
35
client/diff.go
Normal file
35
client/diff.go
Normal file
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
diffapi "github.com/containerd/containerd/v2/api/services/diff/v1"
|
||||
"github.com/containerd/containerd/v2/diff"
|
||||
"github.com/containerd/containerd/v2/diff/proxy"
|
||||
)
|
||||
|
||||
// DiffService handles the computation and application of diffs
|
||||
type DiffService interface {
|
||||
diff.Comparer
|
||||
diff.Applier
|
||||
}
|
||||
|
||||
// NewDiffServiceFromClient returns a new diff service which communicates
|
||||
// over a GRPC connection.
|
||||
func NewDiffServiceFromClient(client diffapi.DiffClient) DiffService {
|
||||
return proxy.NewDiffApplier(client).(DiffService)
|
||||
}
|
||||
123
client/events.go
Normal file
123
client/events.go
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
eventsapi "github.com/containerd/containerd/v2/api/services/events/v1"
|
||||
"github.com/containerd/containerd/v2/errdefs"
|
||||
"github.com/containerd/containerd/v2/events"
|
||||
"github.com/containerd/containerd/v2/protobuf"
|
||||
"github.com/containerd/typeurl/v2"
|
||||
)
|
||||
|
||||
// EventService handles the publish, forward and subscribe of events.
|
||||
type EventService interface {
|
||||
events.Publisher
|
||||
events.Forwarder
|
||||
events.Subscriber
|
||||
}
|
||||
|
||||
// NewEventServiceFromClient returns a new event service which communicates
|
||||
// over a GRPC connection.
|
||||
func NewEventServiceFromClient(client eventsapi.EventsClient) EventService {
|
||||
return &eventRemote{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
type eventRemote struct {
|
||||
client eventsapi.EventsClient
|
||||
}
|
||||
|
||||
func (e *eventRemote) Publish(ctx context.Context, topic string, event events.Event) error {
|
||||
evt, err := typeurl.MarshalAny(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req := &eventsapi.PublishRequest{
|
||||
Topic: topic,
|
||||
Event: protobuf.FromAny(evt),
|
||||
}
|
||||
if _, err := e.client.Publish(ctx, req); err != nil {
|
||||
return errdefs.FromGRPC(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *eventRemote) Forward(ctx context.Context, envelope *events.Envelope) error {
|
||||
req := &eventsapi.ForwardRequest{
|
||||
Envelope: &eventsapi.Envelope{
|
||||
Timestamp: protobuf.ToTimestamp(envelope.Timestamp),
|
||||
Namespace: envelope.Namespace,
|
||||
Topic: envelope.Topic,
|
||||
Event: protobuf.FromAny(envelope.Event),
|
||||
},
|
||||
}
|
||||
if _, err := e.client.Forward(ctx, req); err != nil {
|
||||
return errdefs.FromGRPC(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *eventRemote) Subscribe(ctx context.Context, filters ...string) (ch <-chan *events.Envelope, errs <-chan error) {
|
||||
var (
|
||||
evq = make(chan *events.Envelope)
|
||||
errq = make(chan error, 1)
|
||||
)
|
||||
|
||||
errs = errq
|
||||
ch = evq
|
||||
|
||||
session, err := e.client.Subscribe(ctx, &eventsapi.SubscribeRequest{
|
||||
Filters: filters,
|
||||
})
|
||||
if err != nil {
|
||||
errq <- err
|
||||
close(errq)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer close(errq)
|
||||
|
||||
for {
|
||||
ev, err := session.Recv()
|
||||
if err != nil {
|
||||
errq <- err
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case evq <- &events.Envelope{
|
||||
Timestamp: protobuf.FromTimestamp(ev.Timestamp),
|
||||
Namespace: ev.Namespace,
|
||||
Topic: ev.Topic,
|
||||
Event: ev.Event,
|
||||
}:
|
||||
case <-ctx.Done():
|
||||
if cerr := ctx.Err(); cerr != context.Canceled {
|
||||
errq <- cerr
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return ch, errs
|
||||
}
|
||||
31
client/export.go
Normal file
31
client/export.go
Normal file
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/containerd/containerd/v2/images/archive"
|
||||
)
|
||||
|
||||
// Export exports images to a Tar stream.
|
||||
// The tar archive is in OCI format with a Docker compatible manifest
|
||||
// when a single target platform is given.
|
||||
func (c *Client) Export(ctx context.Context, w io.Writer, opts ...archive.ExportOpt) error {
|
||||
return archive.Export(ctx, c.ContentStore(), w, opts...)
|
||||
}
|
||||
52
client/grpc.go
Normal file
52
client/grpc.go
Normal file
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/containerd/containerd/v2/namespaces"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
type namespaceInterceptor struct {
|
||||
namespace string
|
||||
}
|
||||
|
||||
func (ni namespaceInterceptor) unary(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
|
||||
_, ok := namespaces.Namespace(ctx)
|
||||
if !ok {
|
||||
ctx = namespaces.WithNamespace(ctx, ni.namespace)
|
||||
}
|
||||
return invoker(ctx, method, req, reply, cc, opts...)
|
||||
}
|
||||
|
||||
func (ni namespaceInterceptor) stream(ctx context.Context, desc *grpc.StreamDesc, cc *grpc.ClientConn, method string, streamer grpc.Streamer, opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
_, ok := namespaces.Namespace(ctx)
|
||||
if !ok {
|
||||
ctx = namespaces.WithNamespace(ctx, ni.namespace)
|
||||
}
|
||||
|
||||
return streamer(ctx, desc, cc, method, opts...)
|
||||
}
|
||||
|
||||
func newNSInterceptors(ns string) (grpc.UnaryClientInterceptor, grpc.StreamClientInterceptor) {
|
||||
ni := namespaceInterceptor{
|
||||
namespace: ns,
|
||||
}
|
||||
return grpc.UnaryClientInterceptor(ni.unary), grpc.StreamClientInterceptor(ni.stream)
|
||||
}
|
||||
428
client/image.go
Normal file
428
client/image.go
Normal file
@@ -0,0 +1,428 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/containerd/containerd/v2/content"
|
||||
"github.com/containerd/containerd/v2/diff"
|
||||
"github.com/containerd/containerd/v2/errdefs"
|
||||
"github.com/containerd/containerd/v2/images"
|
||||
"github.com/containerd/containerd/v2/images/usage"
|
||||
"github.com/containerd/containerd/v2/labels"
|
||||
"github.com/containerd/containerd/v2/pkg/kmutex"
|
||||
"github.com/containerd/containerd/v2/platforms"
|
||||
"github.com/containerd/containerd/v2/rootfs"
|
||||
"github.com/containerd/containerd/v2/snapshots"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/opencontainers/image-spec/identity"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// Image describes an image used by containers
|
||||
type Image interface {
|
||||
// Name of the image
|
||||
Name() string
|
||||
// Target descriptor for the image content
|
||||
Target() ocispec.Descriptor
|
||||
// Labels of the image
|
||||
Labels() map[string]string
|
||||
// Unpack unpacks the image's content into a snapshot
|
||||
Unpack(context.Context, string, ...UnpackOpt) error
|
||||
// RootFS returns the unpacked diffids that make up images rootfs.
|
||||
RootFS(ctx context.Context) ([]digest.Digest, error)
|
||||
// Size returns the total size of the image's packed resources.
|
||||
Size(ctx context.Context) (int64, error)
|
||||
// Usage returns a usage calculation for the image.
|
||||
Usage(context.Context, ...UsageOpt) (int64, error)
|
||||
// Config descriptor for the image.
|
||||
Config(ctx context.Context) (ocispec.Descriptor, error)
|
||||
// IsUnpacked returns whether an image is unpacked.
|
||||
IsUnpacked(context.Context, string) (bool, error)
|
||||
// ContentStore provides a content store which contains image blob data
|
||||
ContentStore() content.Store
|
||||
// Metadata returns the underlying image metadata
|
||||
Metadata() images.Image
|
||||
// Platform returns the platform match comparer. Can be nil.
|
||||
Platform() platforms.MatchComparer
|
||||
// Spec returns the OCI image spec for a given image.
|
||||
Spec(ctx context.Context) (ocispec.Image, error)
|
||||
}
|
||||
|
||||
type usageOptions struct {
|
||||
manifestLimit *int
|
||||
manifestOnly bool
|
||||
snapshots bool
|
||||
}
|
||||
|
||||
// UsageOpt is used to configure the usage calculation
|
||||
type UsageOpt func(*usageOptions) error
|
||||
|
||||
// WithUsageManifestLimit sets the limit to the number of manifests which will
|
||||
// be walked for usage. Setting this value to 0 will require all manifests to
|
||||
// be walked, returning ErrNotFound if manifests are missing.
|
||||
// NOTE: By default all manifests which exist will be walked
|
||||
// and any non-existent manifests and their subobjects will be ignored.
|
||||
func WithUsageManifestLimit(i int) UsageOpt {
|
||||
// If 0 then don't filter any manifests
|
||||
// By default limits to current platform
|
||||
return func(o *usageOptions) error {
|
||||
o.manifestLimit = &i
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSnapshotUsage will check for referenced snapshots from the image objects
|
||||
// and include the snapshot size in the total usage.
|
||||
func WithSnapshotUsage() UsageOpt {
|
||||
return func(o *usageOptions) error {
|
||||
o.snapshots = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithManifestUsage is used to get the usage for an image based on what is
|
||||
// reported by the manifests rather than what exists in the content store.
|
||||
// NOTE: This function is best used with the manifest limit set to get a
|
||||
// consistent value, otherwise non-existent manifests will be excluded.
|
||||
func WithManifestUsage() UsageOpt {
|
||||
return func(o *usageOptions) error {
|
||||
o.manifestOnly = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var _ = (Image)(&image{})
|
||||
|
||||
// NewImage returns a client image object from the metadata image
|
||||
func NewImage(client *Client, i images.Image) Image {
|
||||
return &image{
|
||||
client: client,
|
||||
i: i,
|
||||
platform: client.platform,
|
||||
}
|
||||
}
|
||||
|
||||
// NewImageWithPlatform returns a client image object from the metadata image
|
||||
func NewImageWithPlatform(client *Client, i images.Image, platform platforms.MatchComparer) Image {
|
||||
return &image{
|
||||
client: client,
|
||||
i: i,
|
||||
platform: platform,
|
||||
}
|
||||
}
|
||||
|
||||
type image struct {
|
||||
client *Client
|
||||
|
||||
i images.Image
|
||||
platform platforms.MatchComparer
|
||||
diffIDs []digest.Digest
|
||||
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (i *image) Metadata() images.Image {
|
||||
return i.i
|
||||
}
|
||||
|
||||
func (i *image) Name() string {
|
||||
return i.i.Name
|
||||
}
|
||||
|
||||
func (i *image) Target() ocispec.Descriptor {
|
||||
return i.i.Target
|
||||
}
|
||||
|
||||
func (i *image) Labels() map[string]string {
|
||||
return i.i.Labels
|
||||
}
|
||||
|
||||
func (i *image) RootFS(ctx context.Context) ([]digest.Digest, error) {
|
||||
i.mu.Lock()
|
||||
defer i.mu.Unlock()
|
||||
if i.diffIDs != nil {
|
||||
return i.diffIDs, nil
|
||||
}
|
||||
|
||||
provider := i.client.ContentStore()
|
||||
diffIDs, err := i.i.RootFS(ctx, provider, i.platform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
i.diffIDs = diffIDs
|
||||
return diffIDs, nil
|
||||
}
|
||||
|
||||
func (i *image) Size(ctx context.Context) (int64, error) {
|
||||
return usage.CalculateImageUsage(ctx, i.i, i.client.ContentStore(), usage.WithManifestLimit(i.platform, 1), usage.WithManifestUsage())
|
||||
}
|
||||
|
||||
func (i *image) Usage(ctx context.Context, opts ...UsageOpt) (int64, error) {
|
||||
var config usageOptions
|
||||
for _, opt := range opts {
|
||||
if err := opt(&config); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
var usageOpts []usage.Opt
|
||||
if config.manifestLimit != nil {
|
||||
usageOpts = append(usageOpts, usage.WithManifestLimit(i.platform, *config.manifestLimit))
|
||||
}
|
||||
if config.snapshots {
|
||||
usageOpts = append(usageOpts, usage.WithSnapshotters(i.client.SnapshotService))
|
||||
}
|
||||
if config.manifestOnly {
|
||||
usageOpts = append(usageOpts, usage.WithManifestUsage())
|
||||
}
|
||||
|
||||
return usage.CalculateImageUsage(ctx, i.i, i.client.ContentStore(), usageOpts...)
|
||||
}
|
||||
|
||||
func (i *image) Config(ctx context.Context) (ocispec.Descriptor, error) {
|
||||
provider := i.client.ContentStore()
|
||||
return i.i.Config(ctx, provider, i.platform)
|
||||
}
|
||||
|
||||
func (i *image) IsUnpacked(ctx context.Context, snapshotterName string) (bool, error) {
|
||||
sn, err := i.client.getSnapshotter(ctx, snapshotterName)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
diffs, err := i.RootFS(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if _, err := sn.Stat(ctx, identity.ChainID(diffs).String()); err != nil {
|
||||
if errdefs.IsNotFound(err) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (i *image) Spec(ctx context.Context) (ocispec.Image, error) {
|
||||
var ociImage ocispec.Image
|
||||
|
||||
desc, err := i.Config(ctx)
|
||||
if err != nil {
|
||||
return ociImage, fmt.Errorf("get image config descriptor: %w", err)
|
||||
}
|
||||
|
||||
blob, err := content.ReadBlob(ctx, i.ContentStore(), desc)
|
||||
if err != nil {
|
||||
return ociImage, fmt.Errorf("read image config from content store: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(blob, &ociImage); err != nil {
|
||||
return ociImage, fmt.Errorf("unmarshal image config %s: %w", blob, err)
|
||||
}
|
||||
|
||||
return ociImage, nil
|
||||
}
|
||||
|
||||
// UnpackConfig provides configuration for the unpack of an image
|
||||
type UnpackConfig struct {
|
||||
// ApplyOpts for applying a diff to a snapshotter
|
||||
ApplyOpts []diff.ApplyOpt
|
||||
// SnapshotOpts for configuring a snapshotter
|
||||
SnapshotOpts []snapshots.Opt
|
||||
// CheckPlatformSupported is whether to validate that a snapshotter
|
||||
// supports an image's platform before unpacking
|
||||
CheckPlatformSupported bool
|
||||
// DuplicationSuppressor is used to make sure that there is only one
|
||||
// in-flight fetch request or unpack handler for a given descriptor's
|
||||
// digest or chain ID.
|
||||
DuplicationSuppressor kmutex.KeyedLocker
|
||||
}
|
||||
|
||||
// UnpackOpt provides configuration for unpack
|
||||
type UnpackOpt func(context.Context, *UnpackConfig) error
|
||||
|
||||
// WithSnapshotterPlatformCheck sets `CheckPlatformSupported` on the UnpackConfig
|
||||
func WithSnapshotterPlatformCheck() UnpackOpt {
|
||||
return func(ctx context.Context, uc *UnpackConfig) error {
|
||||
uc.CheckPlatformSupported = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithUnpackDuplicationSuppressor sets `DuplicationSuppressor` on the UnpackConfig.
|
||||
func WithUnpackDuplicationSuppressor(suppressor kmutex.KeyedLocker) UnpackOpt {
|
||||
return func(ctx context.Context, uc *UnpackConfig) error {
|
||||
uc.DuplicationSuppressor = suppressor
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (i *image) Unpack(ctx context.Context, snapshotterName string, opts ...UnpackOpt) error {
|
||||
ctx, done, err := i.client.WithLease(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer done(ctx)
|
||||
|
||||
var config UnpackConfig
|
||||
for _, o := range opts {
|
||||
if err := o(ctx, &config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
manifest, err := i.getManifest(ctx, i.platform)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
layers, err := i.getLayers(ctx, manifest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
a = i.client.DiffService()
|
||||
cs = i.client.ContentStore()
|
||||
|
||||
chain []digest.Digest
|
||||
unpacked bool
|
||||
)
|
||||
snapshotterName, err = i.client.resolveSnapshotterName(ctx, snapshotterName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sn, err := i.client.getSnapshotter(ctx, snapshotterName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if config.CheckPlatformSupported {
|
||||
if err := i.checkSnapshotterSupport(ctx, snapshotterName, manifest); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, layer := range layers {
|
||||
unpacked, err = rootfs.ApplyLayerWithOpts(ctx, layer, chain, sn, a, config.SnapshotOpts, config.ApplyOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if unpacked {
|
||||
// Set the uncompressed label after the uncompressed
|
||||
// digest has been verified through apply.
|
||||
cinfo := content.Info{
|
||||
Digest: layer.Blob.Digest,
|
||||
Labels: map[string]string{
|
||||
labels.LabelUncompressed: layer.Diff.Digest.String(),
|
||||
},
|
||||
}
|
||||
if _, err := cs.Update(ctx, cinfo, "labels."+labels.LabelUncompressed); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
chain = append(chain, layer.Diff.Digest)
|
||||
}
|
||||
|
||||
desc, err := i.i.Config(ctx, cs, i.platform)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rootFS := identity.ChainID(chain).String()
|
||||
|
||||
cinfo := content.Info{
|
||||
Digest: desc.Digest,
|
||||
Labels: map[string]string{
|
||||
fmt.Sprintf("containerd.io/gc.ref.snapshot.%s", snapshotterName): rootFS,
|
||||
},
|
||||
}
|
||||
|
||||
_, err = cs.Update(ctx, cinfo, fmt.Sprintf("labels.containerd.io/gc.ref.snapshot.%s", snapshotterName))
|
||||
return err
|
||||
}
|
||||
|
||||
func (i *image) getManifest(ctx context.Context, platform platforms.MatchComparer) (ocispec.Manifest, error) {
|
||||
cs := i.ContentStore()
|
||||
manifest, err := images.Manifest(ctx, cs, i.i.Target, platform)
|
||||
if err != nil {
|
||||
return ocispec.Manifest{}, err
|
||||
}
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func (i *image) getLayers(ctx context.Context, manifest ocispec.Manifest) ([]rootfs.Layer, error) {
|
||||
diffIDs, err := i.RootFS(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve rootfs: %w", err)
|
||||
}
|
||||
|
||||
// parse out the image layers from oci artifact layers
|
||||
imageLayers := []ocispec.Descriptor{}
|
||||
for _, ociLayer := range manifest.Layers {
|
||||
if images.IsLayerType(ociLayer.MediaType) {
|
||||
imageLayers = append(imageLayers, ociLayer)
|
||||
}
|
||||
}
|
||||
if len(diffIDs) != len(imageLayers) {
|
||||
return nil, errors.New("mismatched image rootfs and manifest layers")
|
||||
}
|
||||
layers := make([]rootfs.Layer, len(diffIDs))
|
||||
for i := range diffIDs {
|
||||
layers[i].Diff = ocispec.Descriptor{
|
||||
// TODO: derive media type from compressed type
|
||||
MediaType: ocispec.MediaTypeImageLayer,
|
||||
Digest: diffIDs[i],
|
||||
}
|
||||
layers[i].Blob = imageLayers[i]
|
||||
}
|
||||
return layers, nil
|
||||
}
|
||||
|
||||
func (i *image) checkSnapshotterSupport(ctx context.Context, snapshotterName string, manifest ocispec.Manifest) error {
|
||||
snapshotterPlatformMatcher, err := i.client.GetSnapshotterSupportedPlatforms(ctx, snapshotterName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manifestPlatform, err := images.ConfigPlatform(ctx, i.ContentStore(), manifest.Config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if snapshotterPlatformMatcher.Match(manifestPlatform) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("snapshotter %s does not support platform %s for image %s", snapshotterName, manifestPlatform, manifest.Config.Digest)
|
||||
}
|
||||
|
||||
func (i *image) ContentStore() content.Store {
|
||||
return i.client.ContentStore()
|
||||
}
|
||||
|
||||
func (i *image) Platform() platforms.MatchComparer {
|
||||
return i.platform
|
||||
}
|
||||
149
client/image_store.go
Normal file
149
client/image_store.go
Normal file
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
imagesapi "github.com/containerd/containerd/v2/api/services/images/v1"
|
||||
"github.com/containerd/containerd/v2/errdefs"
|
||||
"github.com/containerd/containerd/v2/images"
|
||||
"github.com/containerd/containerd/v2/oci"
|
||||
"github.com/containerd/containerd/v2/pkg/epoch"
|
||||
"github.com/containerd/containerd/v2/protobuf"
|
||||
ptypes "github.com/containerd/containerd/v2/protobuf/types"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type remoteImages struct {
|
||||
client imagesapi.ImagesClient
|
||||
}
|
||||
|
||||
// NewImageStoreFromClient returns a new image store client
|
||||
func NewImageStoreFromClient(client imagesapi.ImagesClient) images.Store {
|
||||
return &remoteImages{
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *remoteImages) Get(ctx context.Context, name string) (images.Image, error) {
|
||||
resp, err := s.client.Get(ctx, &imagesapi.GetImageRequest{
|
||||
Name: name,
|
||||
})
|
||||
if err != nil {
|
||||
return images.Image{}, errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
return imageFromProto(resp.Image), nil
|
||||
}
|
||||
|
||||
func (s *remoteImages) List(ctx context.Context, filters ...string) ([]images.Image, error) {
|
||||
resp, err := s.client.List(ctx, &imagesapi.ListImagesRequest{
|
||||
Filters: filters,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
return imagesFromProto(resp.Images), nil
|
||||
}
|
||||
|
||||
func (s *remoteImages) Create(ctx context.Context, image images.Image) (images.Image, error) {
|
||||
req := &imagesapi.CreateImageRequest{
|
||||
Image: imageToProto(&image),
|
||||
}
|
||||
if tm := epoch.FromContext(ctx); tm != nil {
|
||||
req.SourceDateEpoch = timestamppb.New(*tm)
|
||||
}
|
||||
created, err := s.client.Create(ctx, req)
|
||||
if err != nil {
|
||||
return images.Image{}, errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
return imageFromProto(created.Image), nil
|
||||
}
|
||||
|
||||
func (s *remoteImages) Update(ctx context.Context, image images.Image, fieldpaths ...string) (images.Image, error) {
|
||||
var updateMask *ptypes.FieldMask
|
||||
if len(fieldpaths) > 0 {
|
||||
updateMask = &ptypes.FieldMask{
|
||||
Paths: fieldpaths,
|
||||
}
|
||||
}
|
||||
req := &imagesapi.UpdateImageRequest{
|
||||
Image: imageToProto(&image),
|
||||
UpdateMask: updateMask,
|
||||
}
|
||||
if tm := epoch.FromContext(ctx); tm != nil {
|
||||
req.SourceDateEpoch = timestamppb.New(*tm)
|
||||
}
|
||||
updated, err := s.client.Update(ctx, req)
|
||||
if err != nil {
|
||||
return images.Image{}, errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
return imageFromProto(updated.Image), nil
|
||||
}
|
||||
|
||||
func (s *remoteImages) Delete(ctx context.Context, name string, opts ...images.DeleteOpt) error {
|
||||
var do images.DeleteOptions
|
||||
for _, opt := range opts {
|
||||
if err := opt(ctx, &do); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
req := &imagesapi.DeleteImageRequest{
|
||||
Name: name,
|
||||
Sync: do.Synchronous,
|
||||
}
|
||||
if do.Target != nil {
|
||||
req.Target = oci.DescriptorToProto(*do.Target)
|
||||
}
|
||||
_, err := s.client.Delete(ctx, req)
|
||||
return errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
func imageToProto(image *images.Image) *imagesapi.Image {
|
||||
return &imagesapi.Image{
|
||||
Name: image.Name,
|
||||
Labels: image.Labels,
|
||||
Target: oci.DescriptorToProto(image.Target),
|
||||
CreatedAt: protobuf.ToTimestamp(image.CreatedAt),
|
||||
UpdatedAt: protobuf.ToTimestamp(image.UpdatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func imageFromProto(imagepb *imagesapi.Image) images.Image {
|
||||
return images.Image{
|
||||
Name: imagepb.Name,
|
||||
Labels: imagepb.Labels,
|
||||
Target: oci.DescriptorFromProto(imagepb.Target),
|
||||
CreatedAt: protobuf.FromTimestamp(imagepb.CreatedAt),
|
||||
UpdatedAt: protobuf.FromTimestamp(imagepb.UpdatedAt),
|
||||
}
|
||||
}
|
||||
|
||||
func imagesFromProto(imagespb []*imagesapi.Image) []images.Image {
|
||||
var images []images.Image
|
||||
|
||||
for _, image := range imagespb {
|
||||
image := image
|
||||
images = append(images, imageFromProto(image))
|
||||
}
|
||||
|
||||
return images
|
||||
}
|
||||
240
client/import.go
Normal file
240
client/import.go
Normal file
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
|
||||
"github.com/containerd/containerd/v2/errdefs"
|
||||
"github.com/containerd/containerd/v2/images"
|
||||
"github.com/containerd/containerd/v2/images/archive"
|
||||
"github.com/containerd/containerd/v2/platforms"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
type importOpts struct {
|
||||
indexName string
|
||||
imageRefT func(string) string
|
||||
dgstRefT func(digest.Digest) string
|
||||
skipDgstRef func(string) bool
|
||||
allPlatforms bool
|
||||
platformMatcher platforms.MatchComparer
|
||||
compress bool
|
||||
discardLayers bool
|
||||
}
|
||||
|
||||
// ImportOpt allows the caller to specify import specific options
|
||||
type ImportOpt func(*importOpts) error
|
||||
|
||||
// WithImageRefTranslator is used to translate the index reference
|
||||
// to an image reference for the image store.
|
||||
func WithImageRefTranslator(f func(string) string) ImportOpt {
|
||||
return func(c *importOpts) error {
|
||||
c.imageRefT = f
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithDigestRef is used to create digest images for each
|
||||
// manifest in the index.
|
||||
func WithDigestRef(f func(digest.Digest) string) ImportOpt {
|
||||
return func(c *importOpts) error {
|
||||
c.dgstRefT = f
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSkipDigestRef is used to specify when to skip applying
|
||||
// WithDigestRef. The callback receives an image reference (or an empty
|
||||
// string if not specified in the image). When the callback returns true,
|
||||
// the skip occurs.
|
||||
func WithSkipDigestRef(f func(string) bool) ImportOpt {
|
||||
return func(c *importOpts) error {
|
||||
c.skipDgstRef = f
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithIndexName creates a tag pointing to the imported index
|
||||
func WithIndexName(name string) ImportOpt {
|
||||
return func(c *importOpts) error {
|
||||
c.indexName = name
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAllPlatforms is used to import content for all platforms.
|
||||
func WithAllPlatforms(allPlatforms bool) ImportOpt {
|
||||
return func(c *importOpts) error {
|
||||
c.allPlatforms = allPlatforms
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithImportPlatform is used to import content for specific platform.
|
||||
func WithImportPlatform(platformMacher platforms.MatchComparer) ImportOpt {
|
||||
return func(c *importOpts) error {
|
||||
c.platformMatcher = platformMacher
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithImportCompression compresses uncompressed layers on import.
|
||||
// This is used for import formats which do not include the manifest.
|
||||
func WithImportCompression() ImportOpt {
|
||||
return func(c *importOpts) error {
|
||||
c.compress = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithDiscardUnpackedLayers allows the garbage collector to clean up
|
||||
// layers from content store after unpacking.
|
||||
func WithDiscardUnpackedLayers() ImportOpt {
|
||||
return func(c *importOpts) error {
|
||||
c.discardLayers = true
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Import imports an image from a Tar stream using reader.
|
||||
// Caller needs to specify importer. Future version may use oci.v1 as the default.
|
||||
// Note that unreferenced blobs may be imported to the content store as well.
|
||||
func (c *Client) Import(ctx context.Context, reader io.Reader, opts ...ImportOpt) ([]images.Image, error) {
|
||||
var iopts importOpts
|
||||
for _, o := range opts {
|
||||
if err := o(&iopts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
ctx, done, err := c.WithLease(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer done(ctx)
|
||||
|
||||
var aio []archive.ImportOpt
|
||||
if iopts.compress {
|
||||
aio = append(aio, archive.WithImportCompression())
|
||||
}
|
||||
|
||||
index, err := archive.ImportIndex(ctx, c.ContentStore(), reader, aio...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
imgs []images.Image
|
||||
cs = c.ContentStore()
|
||||
is = c.ImageService()
|
||||
)
|
||||
|
||||
if iopts.indexName != "" {
|
||||
imgs = append(imgs, images.Image{
|
||||
Name: iopts.indexName,
|
||||
Target: index,
|
||||
})
|
||||
}
|
||||
var platformMatcher = c.platform
|
||||
if iopts.allPlatforms {
|
||||
platformMatcher = platforms.All
|
||||
} else if iopts.platformMatcher != nil {
|
||||
platformMatcher = iopts.platformMatcher
|
||||
}
|
||||
|
||||
var handler images.HandlerFunc = func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||
// Only save images at top level
|
||||
if desc.Digest != index.Digest {
|
||||
return images.Children(ctx, cs, desc)
|
||||
}
|
||||
|
||||
idx, err := decodeIndex(ctx, cs, desc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, m := range idx.Manifests {
|
||||
name := imageName(m.Annotations, iopts.imageRefT)
|
||||
if name != "" {
|
||||
imgs = append(imgs, images.Image{
|
||||
Name: name,
|
||||
Target: m,
|
||||
})
|
||||
}
|
||||
if iopts.skipDgstRef != nil {
|
||||
if iopts.skipDgstRef(name) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if iopts.dgstRefT != nil {
|
||||
ref := iopts.dgstRefT(m.Digest)
|
||||
if ref != "" {
|
||||
imgs = append(imgs, images.Image{
|
||||
Name: ref,
|
||||
Target: m,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return idx.Manifests, nil
|
||||
}
|
||||
|
||||
handler = images.FilterPlatforms(handler, platformMatcher)
|
||||
if iopts.discardLayers {
|
||||
handler = images.SetChildrenMappedLabels(cs, handler, images.ChildGCLabelsFilterLayers)
|
||||
} else {
|
||||
handler = images.SetChildrenLabels(cs, handler)
|
||||
}
|
||||
if err := images.WalkNotEmpty(ctx, handler, index); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := range imgs {
|
||||
img, err := is.Update(ctx, imgs[i], "target")
|
||||
if err != nil {
|
||||
if !errdefs.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
img, err = is.Create(ctx, imgs[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
imgs[i] = img
|
||||
}
|
||||
|
||||
return imgs, nil
|
||||
}
|
||||
|
||||
func imageName(annotations map[string]string, ociCleanup func(string) string) string {
|
||||
name := annotations[images.AnnotationImageName]
|
||||
if name != "" {
|
||||
return name
|
||||
}
|
||||
name = annotations[ocispec.AnnotationRefName]
|
||||
if name != "" {
|
||||
if ociCleanup != nil {
|
||||
name = ociCleanup(name)
|
||||
}
|
||||
}
|
||||
return name
|
||||
}
|
||||
128
client/install.go
Normal file
128
client/install.go
Normal file
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/containerd/v2/archive"
|
||||
"github.com/containerd/containerd/v2/archive/compression"
|
||||
"github.com/containerd/containerd/v2/content"
|
||||
"github.com/containerd/containerd/v2/images"
|
||||
)
|
||||
|
||||
// Install a binary image into the opt service.
|
||||
// More info: https://github.com/containerd/containerd/blob/main/docs/managed-opt.md.
|
||||
func (c *Client) Install(ctx context.Context, image Image, opts ...InstallOpts) error {
|
||||
var config InstallConfig
|
||||
for _, o := range opts {
|
||||
o(&config)
|
||||
}
|
||||
path, err := c.getInstallPath(ctx, config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
cs = image.ContentStore()
|
||||
platform = c.platform
|
||||
)
|
||||
manifest, err := images.Manifest(ctx, cs, image.Target(), platform)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var binDir, libDir string
|
||||
if runtime.GOOS == "windows" {
|
||||
binDir = "Files\\bin"
|
||||
libDir = "Files\\lib"
|
||||
} else {
|
||||
binDir = "bin"
|
||||
libDir = "lib"
|
||||
}
|
||||
for _, layer := range manifest.Layers {
|
||||
ra, err := cs.ReaderAt(ctx, layer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cr := content.NewReader(ra)
|
||||
r, err := compression.DecompressStream(cr)
|
||||
if err != nil {
|
||||
ra.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
filter := archive.WithFilter(func(hdr *tar.Header) (bool, error) {
|
||||
d := filepath.Dir(hdr.Name)
|
||||
result := d == binDir
|
||||
|
||||
if config.Libs {
|
||||
result = result || d == libDir
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
hdr.Name = strings.Replace(hdr.Name, "Files", "", 1)
|
||||
}
|
||||
if result && !config.Replace {
|
||||
if _, err := os.Lstat(filepath.Join(path, hdr.Name)); err == nil {
|
||||
return false, fmt.Errorf("cannot replace %s in %s", hdr.Name, path)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
})
|
||||
|
||||
opts := []archive.ApplyOpt{filter}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
opts = append(opts, archive.WithNoSameOwner())
|
||||
}
|
||||
|
||||
if _, err := archive.Apply(ctx, path, r, opts...); err != nil {
|
||||
r.Close()
|
||||
ra.Close()
|
||||
return err
|
||||
}
|
||||
r.Close()
|
||||
ra.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) getInstallPath(ctx context.Context, config InstallConfig) (string, error) {
|
||||
if config.Path != "" {
|
||||
return config.Path, nil
|
||||
}
|
||||
filters := []string{"id==opt"}
|
||||
resp, err := c.IntrospectionService().Plugins(ctx, filters)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(resp.Plugins) != 1 {
|
||||
return "", errors.New("opt service not enabled")
|
||||
}
|
||||
path := resp.Plugins[0].Exports["path"]
|
||||
if path == "" {
|
||||
return "", errors.New("opt path not exported")
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
47
client/install_opts.go
Normal file
47
client/install_opts.go
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
// InstallOpts configures binary installs
|
||||
type InstallOpts func(*InstallConfig)
|
||||
|
||||
// InstallConfig sets the binary install configuration
|
||||
type InstallConfig struct {
|
||||
// Libs installs libs from the image
|
||||
Libs bool
|
||||
// Replace will overwrite existing binaries or libs in the opt directory
|
||||
Replace bool
|
||||
// Path to install libs and binaries to
|
||||
Path string
|
||||
}
|
||||
|
||||
// WithInstallLibs installs libs from the image
|
||||
func WithInstallLibs(c *InstallConfig) {
|
||||
c.Libs = true
|
||||
}
|
||||
|
||||
// WithInstallReplace will replace existing files
|
||||
func WithInstallReplace(c *InstallConfig) {
|
||||
c.Replace = true
|
||||
}
|
||||
|
||||
// WithInstallPath sets the optional install path
|
||||
func WithInstallPath(path string) InstallOpts {
|
||||
return func(c *InstallConfig) {
|
||||
c.Path = path
|
||||
}
|
||||
}
|
||||
54
client/lease.go
Normal file
54
client/lease.go
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd/v2/leases"
|
||||
)
|
||||
|
||||
// WithLease attaches a lease on the context
|
||||
func (c *Client) WithLease(ctx context.Context, opts ...leases.Opt) (context.Context, func(context.Context) error, error) {
|
||||
nop := func(context.Context) error { return nil }
|
||||
|
||||
_, ok := leases.FromContext(ctx)
|
||||
if ok {
|
||||
return ctx, nop, nil
|
||||
}
|
||||
|
||||
ls := c.LeasesService()
|
||||
|
||||
if len(opts) == 0 {
|
||||
// Use default lease configuration if no options provided
|
||||
opts = []leases.Opt{
|
||||
leases.WithRandomID(),
|
||||
leases.WithExpiration(24 * time.Hour),
|
||||
}
|
||||
}
|
||||
|
||||
l, err := ls.Create(ctx, opts...)
|
||||
if err != nil {
|
||||
return ctx, nop, err
|
||||
}
|
||||
|
||||
ctx = leases.WithLease(ctx, l.ID)
|
||||
return ctx, func(ctx context.Context) error {
|
||||
return ls.Delete(ctx, l)
|
||||
}, nil
|
||||
}
|
||||
121
client/namespaces.go
Normal file
121
client/namespaces.go
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
api "github.com/containerd/containerd/v2/api/services/namespaces/v1"
|
||||
"github.com/containerd/containerd/v2/errdefs"
|
||||
"github.com/containerd/containerd/v2/namespaces"
|
||||
"github.com/containerd/containerd/v2/protobuf/types"
|
||||
)
|
||||
|
||||
// NewNamespaceStoreFromClient returns a new namespace store
|
||||
func NewNamespaceStoreFromClient(client api.NamespacesClient) namespaces.Store {
|
||||
return &remoteNamespaces{client: client}
|
||||
}
|
||||
|
||||
type remoteNamespaces struct {
|
||||
client api.NamespacesClient
|
||||
}
|
||||
|
||||
func (r *remoteNamespaces) Create(ctx context.Context, namespace string, labels map[string]string) error {
|
||||
var req api.CreateNamespaceRequest
|
||||
|
||||
req.Namespace = &api.Namespace{
|
||||
Name: namespace,
|
||||
Labels: labels,
|
||||
}
|
||||
|
||||
_, err := r.client.Create(ctx, &req)
|
||||
if err != nil {
|
||||
return errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *remoteNamespaces) Labels(ctx context.Context, namespace string) (map[string]string, error) {
|
||||
var req api.GetNamespaceRequest
|
||||
req.Name = namespace
|
||||
|
||||
resp, err := r.client.Get(ctx, &req)
|
||||
if err != nil {
|
||||
return nil, errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
return resp.Namespace.Labels, nil
|
||||
}
|
||||
|
||||
func (r *remoteNamespaces) SetLabel(ctx context.Context, namespace, key, value string) error {
|
||||
var req api.UpdateNamespaceRequest
|
||||
|
||||
req.Namespace = &api.Namespace{
|
||||
Name: namespace,
|
||||
Labels: map[string]string{key: value},
|
||||
}
|
||||
|
||||
req.UpdateMask = &types.FieldMask{
|
||||
Paths: []string{strings.Join([]string{"labels", key}, ".")},
|
||||
}
|
||||
|
||||
_, err := r.client.Update(ctx, &req)
|
||||
if err != nil {
|
||||
return errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *remoteNamespaces) List(ctx context.Context) ([]string, error) {
|
||||
var req api.ListNamespacesRequest
|
||||
|
||||
resp, err := r.client.List(ctx, &req)
|
||||
if err != nil {
|
||||
return nil, errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
var namespaces []string
|
||||
|
||||
for _, ns := range resp.Namespaces {
|
||||
namespaces = append(namespaces, ns.Name)
|
||||
}
|
||||
|
||||
return namespaces, nil
|
||||
}
|
||||
|
||||
func (r *remoteNamespaces) Delete(ctx context.Context, namespace string, opts ...namespaces.DeleteOpts) error {
|
||||
i := namespaces.DeleteInfo{
|
||||
Name: namespace,
|
||||
}
|
||||
for _, o := range opts {
|
||||
if err := o(ctx, &i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
req := api.DeleteNamespaceRequest{
|
||||
Name: namespace,
|
||||
}
|
||||
_, err := r.client.Delete(ctx, &req)
|
||||
if err != nil {
|
||||
return errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
26
client/oss_fuzz.go
Normal file
26
client/oss_fuzz.go
Normal file
@@ -0,0 +1,26 @@
|
||||
//go:build gofuzz
|
||||
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"github.com/AdamKorcz/go-118-fuzz-build/testing"
|
||||
)
|
||||
|
||||
// To keep this package in go.mod.
|
||||
var _ = testing.F{}
|
||||
245
client/process.go
Normal file
245
client/process.go
Normal file
@@ -0,0 +1,245 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd/v2/api/services/tasks/v1"
|
||||
"github.com/containerd/containerd/v2/cio"
|
||||
"github.com/containerd/containerd/v2/errdefs"
|
||||
"github.com/containerd/containerd/v2/protobuf"
|
||||
)
|
||||
|
||||
// Process represents a system process
|
||||
type Process interface {
|
||||
// ID of the process
|
||||
ID() string
|
||||
// Pid is the system specific process id
|
||||
Pid() uint32
|
||||
// Start starts the process executing the user's defined binary
|
||||
Start(context.Context) error
|
||||
// Delete removes the process and any resources allocated returning the exit status
|
||||
Delete(context.Context, ...ProcessDeleteOpts) (*ExitStatus, error)
|
||||
// Kill sends the provided signal to the process
|
||||
Kill(context.Context, syscall.Signal, ...KillOpts) error
|
||||
// Wait asynchronously waits for the process to exit, and sends the exit code to the returned channel
|
||||
Wait(context.Context) (<-chan ExitStatus, error)
|
||||
// CloseIO allows various pipes to be closed on the process
|
||||
CloseIO(context.Context, ...IOCloserOpts) error
|
||||
// Resize changes the width and height of the process's terminal
|
||||
Resize(ctx context.Context, w, h uint32) error
|
||||
// IO returns the io set for the process
|
||||
IO() cio.IO
|
||||
// Status returns the executing status of the process
|
||||
Status(context.Context) (Status, error)
|
||||
}
|
||||
|
||||
// NewExitStatus populates an ExitStatus
|
||||
func NewExitStatus(code uint32, t time.Time, err error) *ExitStatus {
|
||||
return &ExitStatus{
|
||||
code: code,
|
||||
exitedAt: t,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// ExitStatus encapsulates a process's exit status.
|
||||
// It is used by `Wait()` to return either a process exit code or an error
|
||||
type ExitStatus struct {
|
||||
code uint32
|
||||
exitedAt time.Time
|
||||
err error
|
||||
}
|
||||
|
||||
// Result returns the exit code and time of the exit status.
|
||||
// An error may be returned here to which indicates there was an error
|
||||
//
|
||||
// at some point while waiting for the exit status. It does not signify
|
||||
// an error with the process itself.
|
||||
//
|
||||
// If an error is returned, the process may still be running.
|
||||
func (s ExitStatus) Result() (uint32, time.Time, error) {
|
||||
return s.code, s.exitedAt, s.err
|
||||
}
|
||||
|
||||
// ExitCode returns the exit code of the process.
|
||||
// This is only valid if Error() returns nil.
|
||||
func (s ExitStatus) ExitCode() uint32 {
|
||||
return s.code
|
||||
}
|
||||
|
||||
// ExitTime returns the exit time of the process
|
||||
// This is only valid if Error() returns nil.
|
||||
func (s ExitStatus) ExitTime() time.Time {
|
||||
return s.exitedAt
|
||||
}
|
||||
|
||||
// Error returns the error, if any, that occurred while waiting for the
|
||||
// process.
|
||||
func (s ExitStatus) Error() error {
|
||||
return s.err
|
||||
}
|
||||
|
||||
type process struct {
|
||||
id string
|
||||
task *task
|
||||
pid uint32
|
||||
io cio.IO
|
||||
}
|
||||
|
||||
func (p *process) ID() string {
|
||||
return p.id
|
||||
}
|
||||
|
||||
// Pid returns the pid of the process
|
||||
// The pid is not set until start is called and returns
|
||||
func (p *process) Pid() uint32 {
|
||||
return p.pid
|
||||
}
|
||||
|
||||
// Start starts the exec process
|
||||
func (p *process) Start(ctx context.Context) error {
|
||||
r, err := p.task.client.TaskService().Start(ctx, &tasks.StartRequest{
|
||||
ContainerID: p.task.id,
|
||||
ExecID: p.id,
|
||||
})
|
||||
if err != nil {
|
||||
if p.io != nil {
|
||||
p.io.Cancel()
|
||||
p.io.Wait()
|
||||
p.io.Close()
|
||||
}
|
||||
return errdefs.FromGRPC(err)
|
||||
}
|
||||
p.pid = r.Pid
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *process) Kill(ctx context.Context, s syscall.Signal, opts ...KillOpts) error {
|
||||
var i KillInfo
|
||||
for _, o := range opts {
|
||||
if err := o(ctx, &i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err := p.task.client.TaskService().Kill(ctx, &tasks.KillRequest{
|
||||
Signal: uint32(s),
|
||||
ContainerID: p.task.id,
|
||||
ExecID: p.id,
|
||||
All: i.All,
|
||||
})
|
||||
return errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
func (p *process) Wait(ctx context.Context) (<-chan ExitStatus, error) {
|
||||
c := make(chan ExitStatus, 1)
|
||||
go func() {
|
||||
defer close(c)
|
||||
r, err := p.task.client.TaskService().Wait(ctx, &tasks.WaitRequest{
|
||||
ContainerID: p.task.id,
|
||||
ExecID: p.id,
|
||||
})
|
||||
if err != nil {
|
||||
c <- ExitStatus{
|
||||
code: UnknownExitStatus,
|
||||
err: err,
|
||||
}
|
||||
return
|
||||
}
|
||||
c <- ExitStatus{
|
||||
code: r.ExitStatus,
|
||||
exitedAt: protobuf.FromTimestamp(r.ExitedAt),
|
||||
}
|
||||
}()
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (p *process) CloseIO(ctx context.Context, opts ...IOCloserOpts) error {
|
||||
r := &tasks.CloseIORequest{
|
||||
ContainerID: p.task.id,
|
||||
ExecID: p.id,
|
||||
}
|
||||
var i IOCloseInfo
|
||||
for _, o := range opts {
|
||||
o(&i)
|
||||
}
|
||||
r.Stdin = i.Stdin
|
||||
_, err := p.task.client.TaskService().CloseIO(ctx, r)
|
||||
return errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
func (p *process) IO() cio.IO {
|
||||
return p.io
|
||||
}
|
||||
|
||||
func (p *process) Resize(ctx context.Context, w, h uint32) error {
|
||||
_, err := p.task.client.TaskService().ResizePty(ctx, &tasks.ResizePtyRequest{
|
||||
ContainerID: p.task.id,
|
||||
Width: w,
|
||||
Height: h,
|
||||
ExecID: p.id,
|
||||
})
|
||||
return errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
func (p *process) Delete(ctx context.Context, opts ...ProcessDeleteOpts) (*ExitStatus, error) {
|
||||
for _, o := range opts {
|
||||
if err := o(ctx, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
status, err := p.Status(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch status.Status {
|
||||
case Running, Paused, Pausing:
|
||||
return nil, fmt.Errorf("current process state: %s, process must be stopped before deletion: %w", status.Status, errdefs.ErrFailedPrecondition)
|
||||
}
|
||||
r, err := p.task.client.TaskService().DeleteProcess(ctx, &tasks.DeleteProcessRequest{
|
||||
ContainerID: p.task.id,
|
||||
ExecID: p.id,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errdefs.FromGRPC(err)
|
||||
}
|
||||
if p.io != nil {
|
||||
p.io.Cancel()
|
||||
p.io.Wait()
|
||||
p.io.Close()
|
||||
}
|
||||
return &ExitStatus{code: r.ExitStatus, exitedAt: protobuf.FromTimestamp(r.ExitedAt)}, nil
|
||||
}
|
||||
|
||||
func (p *process) Status(ctx context.Context) (Status, error) {
|
||||
r, err := p.task.client.TaskService().Get(ctx, &tasks.GetRequest{
|
||||
ContainerID: p.task.id,
|
||||
ExecID: p.id,
|
||||
})
|
||||
if err != nil {
|
||||
return Status{}, errdefs.FromGRPC(err)
|
||||
}
|
||||
return Status{
|
||||
Status: ProcessStatus(strings.ToLower(r.Process.Status.String())),
|
||||
ExitStatus: r.Process.ExitStatus,
|
||||
}, nil
|
||||
}
|
||||
317
client/pull.go
Normal file
317
client/pull.go
Normal file
@@ -0,0 +1,317 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"golang.org/x/sync/semaphore"
|
||||
|
||||
"github.com/containerd/containerd/v2/errdefs"
|
||||
"github.com/containerd/containerd/v2/images"
|
||||
"github.com/containerd/containerd/v2/pkg/unpack"
|
||||
"github.com/containerd/containerd/v2/platforms"
|
||||
"github.com/containerd/containerd/v2/remotes"
|
||||
"github.com/containerd/containerd/v2/remotes/docker"
|
||||
"github.com/containerd/containerd/v2/remotes/docker/schema1" //nolint:staticcheck // Ignore SA1019. Need to keep deprecated package for compatibility.
|
||||
"github.com/containerd/containerd/v2/tracing"
|
||||
)
|
||||
|
||||
const (
|
||||
pullSpanPrefix = "pull"
|
||||
)
|
||||
|
||||
// Pull downloads the provided content into containerd's content store
|
||||
// and returns a platform specific image object
|
||||
func (c *Client) Pull(ctx context.Context, ref string, opts ...RemoteOpt) (_ Image, retErr error) {
|
||||
ctx, span := tracing.StartSpan(ctx, tracing.Name(pullSpanPrefix, "Pull"))
|
||||
defer span.End()
|
||||
|
||||
pullCtx := defaultRemoteContext()
|
||||
|
||||
for _, o := range opts {
|
||||
if err := o(c, pullCtx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if pullCtx.PlatformMatcher == nil {
|
||||
if len(pullCtx.Platforms) > 1 {
|
||||
return nil, errors.New("cannot pull multiplatform image locally, try Fetch")
|
||||
} else if len(pullCtx.Platforms) == 0 {
|
||||
pullCtx.PlatformMatcher = c.platform
|
||||
} else {
|
||||
p, err := platforms.Parse(pullCtx.Platforms[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid platform %s: %w", pullCtx.Platforms[0], err)
|
||||
}
|
||||
|
||||
pullCtx.PlatformMatcher = platforms.Only(p)
|
||||
}
|
||||
}
|
||||
|
||||
span.SetAttributes(
|
||||
tracing.Attribute("image.ref", ref),
|
||||
tracing.Attribute("unpack", pullCtx.Unpack),
|
||||
tracing.Attribute("max.concurrent.downloads", pullCtx.MaxConcurrentDownloads),
|
||||
tracing.Attribute("platforms.count", len(pullCtx.Platforms)),
|
||||
)
|
||||
|
||||
ctx, done, err := c.WithLease(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer done(ctx)
|
||||
|
||||
var unpacker *unpack.Unpacker
|
||||
|
||||
if pullCtx.Unpack {
|
||||
snapshotterName, err := c.resolveSnapshotterName(ctx, pullCtx.Snapshotter)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to resolve snapshotter: %w", err)
|
||||
}
|
||||
span.SetAttributes(tracing.Attribute("snapshotter.name", snapshotterName))
|
||||
var uconfig UnpackConfig
|
||||
for _, opt := range pullCtx.UnpackOpts {
|
||||
if err := opt(ctx, &uconfig); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var platformMatcher platforms.Matcher
|
||||
if !uconfig.CheckPlatformSupported {
|
||||
platformMatcher = platforms.All
|
||||
}
|
||||
|
||||
// Check client Unpack config
|
||||
platform := unpack.Platform{
|
||||
Platform: platformMatcher,
|
||||
SnapshotterKey: snapshotterName,
|
||||
Snapshotter: c.SnapshotService(snapshotterName),
|
||||
SnapshotOpts: append(pullCtx.SnapshotterOpts, uconfig.SnapshotOpts...),
|
||||
Applier: c.DiffService(),
|
||||
ApplyOpts: uconfig.ApplyOpts,
|
||||
}
|
||||
uopts := []unpack.UnpackerOpt{unpack.WithUnpackPlatform(platform)}
|
||||
if pullCtx.MaxConcurrentDownloads > 0 {
|
||||
uopts = append(uopts, unpack.WithLimiter(semaphore.NewWeighted(int64(pullCtx.MaxConcurrentDownloads))))
|
||||
}
|
||||
if uconfig.DuplicationSuppressor != nil {
|
||||
uopts = append(uopts, unpack.WithDuplicationSuppressor(uconfig.DuplicationSuppressor))
|
||||
}
|
||||
unpacker, err = unpack.NewUnpacker(ctx, c.ContentStore(), uopts...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to initialize unpacker: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if _, err := unpacker.Wait(); err != nil {
|
||||
if retErr == nil {
|
||||
retErr = fmt.Errorf("unpack: %w", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
wrapper := pullCtx.HandlerWrapper
|
||||
pullCtx.HandlerWrapper = func(h images.Handler) images.Handler {
|
||||
if wrapper == nil {
|
||||
return unpacker.Unpack(h)
|
||||
}
|
||||
return unpacker.Unpack(wrapper(h))
|
||||
}
|
||||
}
|
||||
|
||||
img, err := c.fetch(ctx, pullCtx, ref, 1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// NOTE(fuweid): unpacker defers blobs download. before create image
|
||||
// record in ImageService, should wait for unpacking(including blobs
|
||||
// download).
|
||||
var ur unpack.Result
|
||||
if unpacker != nil {
|
||||
_, unpackSpan := tracing.StartSpan(ctx, tracing.Name(pullSpanPrefix, "UnpackWait"))
|
||||
if ur, err = unpacker.Wait(); err != nil {
|
||||
unpackSpan.SetStatus(err)
|
||||
unpackSpan.End()
|
||||
return nil, err
|
||||
}
|
||||
unpackSpan.End()
|
||||
}
|
||||
|
||||
img, err = c.createNewImage(ctx, img)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
i := NewImageWithPlatform(c, img, pullCtx.PlatformMatcher)
|
||||
span.SetAttributes(tracing.Attribute("image.ref", i.Name()))
|
||||
|
||||
if unpacker != nil && ur.Unpacks == 0 {
|
||||
// Unpack was tried previously but nothing was unpacked
|
||||
// This is at least required for schema 1 image.
|
||||
if err := i.Unpack(ctx, pullCtx.Snapshotter, pullCtx.UnpackOpts...); err != nil {
|
||||
return nil, fmt.Errorf("failed to unpack image on snapshotter %s: %w", pullCtx.Snapshotter, err)
|
||||
}
|
||||
}
|
||||
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (c *Client) fetch(ctx context.Context, rCtx *RemoteContext, ref string, limit int) (images.Image, error) {
|
||||
ctx, span := tracing.StartSpan(ctx, tracing.Name(pullSpanPrefix, "fetch"))
|
||||
defer span.End()
|
||||
store := c.ContentStore()
|
||||
name, desc, err := rCtx.Resolver.Resolve(ctx, ref)
|
||||
if err != nil {
|
||||
return images.Image{}, fmt.Errorf("failed to resolve reference %q: %w", ref, err)
|
||||
}
|
||||
|
||||
fetcher, err := rCtx.Resolver.Fetcher(ctx, name)
|
||||
if err != nil {
|
||||
return images.Image{}, fmt.Errorf("failed to get fetcher for %q: %w", name, err)
|
||||
}
|
||||
|
||||
var (
|
||||
handler images.Handler
|
||||
|
||||
isConvertible bool
|
||||
originalSchema1Digest string
|
||||
converterFunc func(context.Context, ocispec.Descriptor) (ocispec.Descriptor, error)
|
||||
limiter *semaphore.Weighted
|
||||
)
|
||||
|
||||
if desc.MediaType == images.MediaTypeDockerSchema1Manifest && rCtx.ConvertSchema1 {
|
||||
schema1Converter := schema1.NewConverter(store, fetcher)
|
||||
|
||||
handler = images.Handlers(append(rCtx.BaseHandlers, schema1Converter)...)
|
||||
|
||||
isConvertible = true
|
||||
|
||||
converterFunc = func(ctx context.Context, _ ocispec.Descriptor) (ocispec.Descriptor, error) {
|
||||
return schema1Converter.Convert(ctx)
|
||||
}
|
||||
|
||||
originalSchema1Digest = desc.Digest.String()
|
||||
} else {
|
||||
// Get all the children for a descriptor
|
||||
childrenHandler := images.ChildrenHandler(store)
|
||||
// Set any children labels for that content
|
||||
childrenHandler = images.SetChildrenMappedLabels(store, childrenHandler, rCtx.ChildLabelMap)
|
||||
if rCtx.AllMetadata {
|
||||
// Filter manifests by platforms but allow to handle manifest
|
||||
// and configuration for not-target platforms
|
||||
childrenHandler = remotes.FilterManifestByPlatformHandler(childrenHandler, rCtx.PlatformMatcher)
|
||||
} else {
|
||||
// Filter children by platforms if specified.
|
||||
childrenHandler = images.FilterPlatforms(childrenHandler, rCtx.PlatformMatcher)
|
||||
}
|
||||
// Sort and limit manifests if a finite number is needed
|
||||
if limit > 0 {
|
||||
childrenHandler = images.LimitManifests(childrenHandler, rCtx.PlatformMatcher, limit)
|
||||
}
|
||||
|
||||
// set isConvertible to true if there is application/octet-stream media type
|
||||
convertibleHandler := images.HandlerFunc(
|
||||
func(_ context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
|
||||
if desc.MediaType == docker.LegacyConfigMediaType {
|
||||
isConvertible = true
|
||||
}
|
||||
|
||||
return []ocispec.Descriptor{}, nil
|
||||
},
|
||||
)
|
||||
|
||||
appendDistSrcLabelHandler, err := docker.AppendDistributionSourceLabel(store, ref)
|
||||
if err != nil {
|
||||
return images.Image{}, err
|
||||
}
|
||||
|
||||
handlers := append(rCtx.BaseHandlers,
|
||||
remotes.FetchHandler(store, fetcher),
|
||||
convertibleHandler,
|
||||
childrenHandler,
|
||||
appendDistSrcLabelHandler,
|
||||
)
|
||||
|
||||
handler = images.Handlers(handlers...)
|
||||
|
||||
converterFunc = func(ctx context.Context, desc ocispec.Descriptor) (ocispec.Descriptor, error) {
|
||||
return docker.ConvertManifest(ctx, store, desc)
|
||||
}
|
||||
}
|
||||
|
||||
if rCtx.HandlerWrapper != nil {
|
||||
handler = rCtx.HandlerWrapper(handler)
|
||||
}
|
||||
|
||||
if rCtx.MaxConcurrentDownloads > 0 {
|
||||
limiter = semaphore.NewWeighted(int64(rCtx.MaxConcurrentDownloads))
|
||||
}
|
||||
|
||||
if err := images.Dispatch(ctx, handler, limiter, desc); err != nil {
|
||||
return images.Image{}, err
|
||||
}
|
||||
|
||||
if isConvertible {
|
||||
if desc, err = converterFunc(ctx, desc); err != nil {
|
||||
return images.Image{}, err
|
||||
}
|
||||
}
|
||||
|
||||
if originalSchema1Digest != "" {
|
||||
if rCtx.Labels == nil {
|
||||
rCtx.Labels = make(map[string]string)
|
||||
}
|
||||
rCtx.Labels[images.ConvertedDockerSchema1LabelKey] = originalSchema1Digest
|
||||
}
|
||||
|
||||
return images.Image{
|
||||
Name: name,
|
||||
Target: desc,
|
||||
Labels: rCtx.Labels,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *Client) createNewImage(ctx context.Context, img images.Image) (images.Image, error) {
|
||||
ctx, span := tracing.StartSpan(ctx, tracing.Name(pullSpanPrefix, "pull.createNewImage"))
|
||||
defer span.End()
|
||||
is := c.ImageService()
|
||||
for {
|
||||
if created, err := is.Create(ctx, img); err != nil {
|
||||
if !errdefs.IsAlreadyExists(err) {
|
||||
return images.Image{}, err
|
||||
}
|
||||
|
||||
updated, err := is.Update(ctx, img)
|
||||
if err != nil {
|
||||
// if image was removed, try create again
|
||||
if errdefs.IsNotFound(err) {
|
||||
continue
|
||||
}
|
||||
return images.Image{}, err
|
||||
}
|
||||
|
||||
img = updated
|
||||
} else {
|
||||
img = created
|
||||
}
|
||||
|
||||
return img, nil
|
||||
}
|
||||
}
|
||||
248
client/sandbox.go
Normal file
248
client/sandbox.go
Normal file
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd/v2/containers"
|
||||
"github.com/containerd/containerd/v2/errdefs"
|
||||
"github.com/containerd/containerd/v2/oci"
|
||||
"github.com/containerd/containerd/v2/protobuf/types"
|
||||
api "github.com/containerd/containerd/v2/sandbox"
|
||||
"github.com/containerd/typeurl/v2"
|
||||
)
|
||||
|
||||
// Sandbox is a high level client to containerd's sandboxes.
|
||||
type Sandbox interface {
|
||||
// ID is a sandbox identifier
|
||||
ID() string
|
||||
// PID returns sandbox's process PID or error if its not yet started.
|
||||
PID() (uint32, error)
|
||||
// NewContainer creates new container that will belong to this sandbox
|
||||
NewContainer(ctx context.Context, id string, opts ...NewContainerOpts) (Container, error)
|
||||
// Labels returns the labels set on the sandbox
|
||||
Labels(ctx context.Context) (map[string]string, error)
|
||||
// Start starts new sandbox instance
|
||||
Start(ctx context.Context) error
|
||||
// Stop sends stop request to the shim instance.
|
||||
Stop(ctx context.Context) error
|
||||
// Wait blocks until sandbox process exits.
|
||||
Wait(ctx context.Context) (<-chan ExitStatus, error)
|
||||
// Shutdown removes sandbox from the metadata store and shutdowns shim instance.
|
||||
Shutdown(ctx context.Context) error
|
||||
}
|
||||
|
||||
type sandboxClient struct {
|
||||
pid *uint32
|
||||
client *Client
|
||||
metadata api.Sandbox
|
||||
}
|
||||
|
||||
func (s *sandboxClient) ID() string {
|
||||
return s.metadata.ID
|
||||
}
|
||||
|
||||
func (s *sandboxClient) PID() (uint32, error) {
|
||||
if s.pid == nil {
|
||||
return 0, fmt.Errorf("sandbox not started")
|
||||
}
|
||||
|
||||
return *s.pid, nil
|
||||
}
|
||||
|
||||
func (s *sandboxClient) NewContainer(ctx context.Context, id string, opts ...NewContainerOpts) (Container, error) {
|
||||
return s.client.NewContainer(ctx, id, append(opts, WithSandbox(s.ID()))...)
|
||||
}
|
||||
|
||||
func (s *sandboxClient) Labels(ctx context.Context) (map[string]string, error) {
|
||||
sandbox, err := s.client.SandboxStore().Get(ctx, s.ID())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return sandbox.Labels, nil
|
||||
}
|
||||
|
||||
func (s *sandboxClient) Start(ctx context.Context) error {
|
||||
resp, err := s.client.SandboxController(s.metadata.Sandboxer).Start(ctx, s.ID())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.pid = &resp.Pid
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sandboxClient) Wait(ctx context.Context) (<-chan ExitStatus, error) {
|
||||
c := make(chan ExitStatus, 1)
|
||||
go func() {
|
||||
defer close(c)
|
||||
|
||||
exitStatus, err := s.client.SandboxController(s.metadata.Sandboxer).Wait(ctx, s.ID())
|
||||
if err != nil {
|
||||
c <- ExitStatus{
|
||||
code: UnknownExitStatus,
|
||||
err: err,
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
c <- ExitStatus{
|
||||
code: exitStatus.ExitStatus,
|
||||
exitedAt: exitStatus.ExitedAt,
|
||||
}
|
||||
}()
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (s *sandboxClient) Stop(ctx context.Context) error {
|
||||
return s.client.SandboxController(s.metadata.Sandboxer).Stop(ctx, s.ID())
|
||||
}
|
||||
|
||||
func (s *sandboxClient) Shutdown(ctx context.Context) error {
|
||||
if err := s.client.SandboxController(s.metadata.Sandboxer).Shutdown(ctx, s.ID()); err != nil && errdefs.IsNotFound(err) {
|
||||
return fmt.Errorf("failed to shutdown sandbox: %w", err)
|
||||
}
|
||||
|
||||
if err := s.client.SandboxStore().Delete(ctx, s.ID()); err != nil && !errdefs.IsNotFound(err) {
|
||||
return fmt.Errorf("failed to delete sandbox from store: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewSandbox creates new sandbox client
|
||||
func (c *Client) NewSandbox(ctx context.Context, sandboxID string, opts ...NewSandboxOpts) (Sandbox, error) {
|
||||
if sandboxID == "" {
|
||||
return nil, errors.New("sandbox ID must be specified")
|
||||
}
|
||||
|
||||
newSandbox := api.Sandbox{
|
||||
ID: sandboxID,
|
||||
CreatedAt: time.Now().UTC(),
|
||||
UpdatedAt: time.Now().UTC(),
|
||||
}
|
||||
|
||||
for _, opt := range opts {
|
||||
if err := opt(ctx, c, &newSandbox); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
metadata, err := c.SandboxStore().Create(ctx, newSandbox)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &sandboxClient{
|
||||
pid: nil, // Not yet started
|
||||
client: c,
|
||||
metadata: metadata,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// LoadSandbox laods existing sandbox metadata object using the id
|
||||
func (c *Client) LoadSandbox(ctx context.Context, id string) (Sandbox, error) {
|
||||
sandbox, err := c.SandboxStore().Get(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
status, err := c.SandboxController(sandbox.Sandboxer).Status(ctx, id, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load sandbox %s, status request failed: %w", id, err)
|
||||
}
|
||||
|
||||
return &sandboxClient{
|
||||
pid: &status.Pid,
|
||||
client: c,
|
||||
metadata: sandbox,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewSandboxOpts is a sandbox options and extensions to be provided by client
|
||||
type NewSandboxOpts func(ctx context.Context, client *Client, sandbox *api.Sandbox) error
|
||||
|
||||
// WithSandboxRuntime allows a user to specify the runtime to be used to run a sandbox
|
||||
func WithSandboxRuntime(name string, options interface{}) NewSandboxOpts {
|
||||
return func(ctx context.Context, client *Client, s *api.Sandbox) error {
|
||||
if options == nil {
|
||||
options = &types.Empty{}
|
||||
}
|
||||
|
||||
opts, err := typeurl.MarshalAny(options)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal sandbox runtime options: %w", err)
|
||||
}
|
||||
|
||||
s.Runtime = api.RuntimeOpts{
|
||||
Name: name,
|
||||
Options: opts,
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSandboxSpec will provide the sandbox runtime spec
|
||||
func WithSandboxSpec(s *oci.Spec, opts ...oci.SpecOpts) NewSandboxOpts {
|
||||
return func(ctx context.Context, client *Client, sandbox *api.Sandbox) error {
|
||||
c := &containers.Container{ID: sandbox.ID}
|
||||
|
||||
if err := oci.ApplyOpts(ctx, client, c, s, opts...); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
spec, err := typeurl.MarshalAny(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal spec: %w", err)
|
||||
}
|
||||
|
||||
sandbox.Spec = spec
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSandboxExtension attaches an extension to sandbox
|
||||
func WithSandboxExtension(name string, extension interface{}) NewSandboxOpts {
|
||||
return func(ctx context.Context, client *Client, s *api.Sandbox) error {
|
||||
if s.Extensions == nil {
|
||||
s.Extensions = make(map[string]typeurl.Any)
|
||||
}
|
||||
|
||||
ext, err := typeurl.MarshalAny(extension)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal sandbox extension: %w", err)
|
||||
}
|
||||
|
||||
s.Extensions[name] = ext
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithSandboxLabels attaches map of labels to sandbox
|
||||
func WithSandboxLabels(labels map[string]string) NewSandboxOpts {
|
||||
return func(ctx context.Context, client *Client, sandbox *api.Sandbox) error {
|
||||
sandbox.Labels = labels
|
||||
return nil
|
||||
}
|
||||
}
|
||||
260
client/services.go
Normal file
260
client/services.go
Normal file
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
containersapi "github.com/containerd/containerd/v2/api/services/containers/v1"
|
||||
"github.com/containerd/containerd/v2/api/services/diff/v1"
|
||||
imagesapi "github.com/containerd/containerd/v2/api/services/images/v1"
|
||||
introspectionapi "github.com/containerd/containerd/v2/api/services/introspection/v1"
|
||||
namespacesapi "github.com/containerd/containerd/v2/api/services/namespaces/v1"
|
||||
"github.com/containerd/containerd/v2/api/services/tasks/v1"
|
||||
"github.com/containerd/containerd/v2/containers"
|
||||
"github.com/containerd/containerd/v2/content"
|
||||
"github.com/containerd/containerd/v2/images"
|
||||
"github.com/containerd/containerd/v2/leases"
|
||||
"github.com/containerd/containerd/v2/namespaces"
|
||||
"github.com/containerd/containerd/v2/plugin"
|
||||
"github.com/containerd/containerd/v2/plugins"
|
||||
"github.com/containerd/containerd/v2/sandbox"
|
||||
srv "github.com/containerd/containerd/v2/services"
|
||||
"github.com/containerd/containerd/v2/services/introspection"
|
||||
"github.com/containerd/containerd/v2/snapshots"
|
||||
)
|
||||
|
||||
type services struct {
|
||||
contentStore content.Store
|
||||
imageStore images.Store
|
||||
containerStore containers.Store
|
||||
namespaceStore namespaces.Store
|
||||
snapshotters map[string]snapshots.Snapshotter
|
||||
taskService tasks.TasksClient
|
||||
diffService DiffService
|
||||
eventService EventService
|
||||
leasesService leases.Manager
|
||||
introspectionService introspection.Service
|
||||
sandboxStore sandbox.Store
|
||||
sandboxers map[string]sandbox.Controller
|
||||
}
|
||||
|
||||
// ServicesOpt allows callers to set options on the services
|
||||
type ServicesOpt func(c *services)
|
||||
|
||||
// WithContentStore sets the content store.
|
||||
func WithContentStore(contentStore content.Store) ServicesOpt {
|
||||
return func(s *services) {
|
||||
s.contentStore = contentStore
|
||||
}
|
||||
}
|
||||
|
||||
// WithImageClient sets the image service to use using an images client.
|
||||
func WithImageClient(imageService imagesapi.ImagesClient) ServicesOpt {
|
||||
return func(s *services) {
|
||||
s.imageStore = NewImageStoreFromClient(imageService)
|
||||
}
|
||||
}
|
||||
|
||||
// WithImageStore sets the image store.
|
||||
func WithImageStore(imageStore images.Store) ServicesOpt {
|
||||
return func(s *services) {
|
||||
s.imageStore = imageStore
|
||||
}
|
||||
}
|
||||
|
||||
// WithSnapshotters sets the snapshotters.
|
||||
func WithSnapshotters(snapshotters map[string]snapshots.Snapshotter) ServicesOpt {
|
||||
return func(s *services) {
|
||||
s.snapshotters = make(map[string]snapshots.Snapshotter)
|
||||
for n, sn := range snapshotters {
|
||||
s.snapshotters[n] = sn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithSandboxers sets the sandbox controllers.
|
||||
func WithSandboxers(sandboxers map[string]sandbox.Controller) ServicesOpt {
|
||||
return func(s *services) {
|
||||
s.sandboxers = make(map[string]sandbox.Controller)
|
||||
for n, sn := range sandboxers {
|
||||
s.sandboxers[n] = sn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithContainerClient sets the container service to use using a containers client.
|
||||
func WithContainerClient(containerService containersapi.ContainersClient) ServicesOpt {
|
||||
return func(s *services) {
|
||||
s.containerStore = NewRemoteContainerStore(containerService)
|
||||
}
|
||||
}
|
||||
|
||||
// WithContainerStore sets the container store.
|
||||
func WithContainerStore(containerStore containers.Store) ServicesOpt {
|
||||
return func(s *services) {
|
||||
s.containerStore = containerStore
|
||||
}
|
||||
}
|
||||
|
||||
// WithTaskClient sets the task service to use from a tasks client.
|
||||
func WithTaskClient(taskService tasks.TasksClient) ServicesOpt {
|
||||
return func(s *services) {
|
||||
s.taskService = taskService
|
||||
}
|
||||
}
|
||||
|
||||
// WithDiffClient sets the diff service to use from a diff client.
|
||||
func WithDiffClient(diffService diff.DiffClient) ServicesOpt {
|
||||
return func(s *services) {
|
||||
s.diffService = NewDiffServiceFromClient(diffService)
|
||||
}
|
||||
}
|
||||
|
||||
// WithDiffService sets the diff store.
|
||||
func WithDiffService(diffService DiffService) ServicesOpt {
|
||||
return func(s *services) {
|
||||
s.diffService = diffService
|
||||
}
|
||||
}
|
||||
|
||||
// WithEventService sets the event service.
|
||||
func WithEventService(eventService EventService) ServicesOpt {
|
||||
return func(s *services) {
|
||||
s.eventService = eventService
|
||||
}
|
||||
}
|
||||
|
||||
// WithNamespaceClient sets the namespace service using a namespaces client.
|
||||
func WithNamespaceClient(namespaceService namespacesapi.NamespacesClient) ServicesOpt {
|
||||
return func(s *services) {
|
||||
s.namespaceStore = NewNamespaceStoreFromClient(namespaceService)
|
||||
}
|
||||
}
|
||||
|
||||
// WithNamespaceService sets the namespace service.
|
||||
func WithNamespaceService(namespaceService namespaces.Store) ServicesOpt {
|
||||
return func(s *services) {
|
||||
s.namespaceStore = namespaceService
|
||||
}
|
||||
}
|
||||
|
||||
// WithLeasesService sets the lease service.
|
||||
func WithLeasesService(leasesService leases.Manager) ServicesOpt {
|
||||
return func(s *services) {
|
||||
s.leasesService = leasesService
|
||||
}
|
||||
}
|
||||
|
||||
// WithIntrospectionClient sets the introspection service using an introspection client.
|
||||
func WithIntrospectionClient(in introspectionapi.IntrospectionClient) ServicesOpt {
|
||||
return func(s *services) {
|
||||
s.introspectionService = introspection.NewIntrospectionServiceFromClient(in)
|
||||
}
|
||||
}
|
||||
|
||||
// WithIntrospectionService sets the introspection service.
|
||||
func WithIntrospectionService(in introspection.Service) ServicesOpt {
|
||||
return func(s *services) {
|
||||
s.introspectionService = in
|
||||
}
|
||||
}
|
||||
|
||||
// WithSandboxStore sets the sandbox store.
|
||||
func WithSandboxStore(client sandbox.Store) ServicesOpt {
|
||||
return func(s *services) {
|
||||
s.sandboxStore = client
|
||||
}
|
||||
}
|
||||
|
||||
// WithInMemoryServices is suitable for cases when there is need to use containerd's client from
|
||||
// another (in-memory) containerd plugin (such as CRI).
|
||||
func WithInMemoryServices(ic *plugin.InitContext) ClientOpt {
|
||||
return func(c *clientOpts) error {
|
||||
var opts []ServicesOpt
|
||||
for t, fn := range map[plugin.Type]func(interface{}) ServicesOpt{
|
||||
plugins.EventPlugin: func(i interface{}) ServicesOpt {
|
||||
return WithEventService(i.(EventService))
|
||||
},
|
||||
plugins.LeasePlugin: func(i interface{}) ServicesOpt {
|
||||
return WithLeasesService(i.(leases.Manager))
|
||||
},
|
||||
plugins.SandboxStorePlugin: func(i interface{}) ServicesOpt {
|
||||
return WithSandboxStore(i.(sandbox.Store))
|
||||
},
|
||||
} {
|
||||
i, err := ic.Get(t)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get %q plugin: %w", t, err)
|
||||
}
|
||||
opts = append(opts, fn(i))
|
||||
}
|
||||
|
||||
plugins, err := ic.GetByType(plugins.ServicePlugin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get service plugin: %w", err)
|
||||
}
|
||||
for s, fn := range map[string]func(interface{}) ServicesOpt{
|
||||
srv.ContentService: func(s interface{}) ServicesOpt {
|
||||
return WithContentStore(s.(content.Store))
|
||||
},
|
||||
srv.ImagesService: func(s interface{}) ServicesOpt {
|
||||
return WithImageClient(s.(imagesapi.ImagesClient))
|
||||
},
|
||||
srv.SnapshotsService: func(s interface{}) ServicesOpt {
|
||||
return WithSnapshotters(s.(map[string]snapshots.Snapshotter))
|
||||
},
|
||||
srv.SandboxControllersService: func(s interface{}) ServicesOpt {
|
||||
return WithSandboxers(s.(map[string]sandbox.Controller))
|
||||
},
|
||||
srv.ContainersService: func(s interface{}) ServicesOpt {
|
||||
return WithContainerClient(s.(containersapi.ContainersClient))
|
||||
},
|
||||
srv.TasksService: func(s interface{}) ServicesOpt {
|
||||
return WithTaskClient(s.(tasks.TasksClient))
|
||||
},
|
||||
srv.DiffService: func(s interface{}) ServicesOpt {
|
||||
return WithDiffClient(s.(diff.DiffClient))
|
||||
},
|
||||
srv.NamespacesService: func(s interface{}) ServicesOpt {
|
||||
return WithNamespaceClient(s.(namespacesapi.NamespacesClient))
|
||||
},
|
||||
srv.IntrospectionService: func(s interface{}) ServicesOpt {
|
||||
return WithIntrospectionClient(s.(introspectionapi.IntrospectionClient))
|
||||
},
|
||||
} {
|
||||
p := plugins[s]
|
||||
if p == nil {
|
||||
return fmt.Errorf("service %q not found", s)
|
||||
}
|
||||
i, err := p.Instance()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get instance of service %q: %w", s, err)
|
||||
}
|
||||
if i == nil {
|
||||
return fmt.Errorf("instance of service %q not found", s)
|
||||
}
|
||||
opts = append(opts, fn(i))
|
||||
}
|
||||
|
||||
c.services = &services{}
|
||||
for _, o := range opts {
|
||||
o(c.services)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
92
client/signals.go
Normal file
92
client/signals.go
Normal file
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"syscall"
|
||||
|
||||
"github.com/containerd/containerd/v2/content"
|
||||
"github.com/containerd/containerd/v2/images"
|
||||
"github.com/moby/sys/signal"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
// StopSignalLabel is a well-known containerd label for storing the stop
|
||||
// signal specified in the OCI image config
|
||||
const StopSignalLabel = "io.containerd.image.config.stop-signal"
|
||||
|
||||
// GetStopSignal retrieves the container stop signal, specified by the
|
||||
// well-known containerd label (StopSignalLabel)
|
||||
func GetStopSignal(ctx context.Context, container Container, defaultSignal syscall.Signal) (syscall.Signal, error) {
|
||||
labels, err := container.Labels(ctx)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
if stopSignal, ok := labels[StopSignalLabel]; ok {
|
||||
return signal.ParseSignal(stopSignal)
|
||||
}
|
||||
|
||||
return defaultSignal, nil
|
||||
}
|
||||
|
||||
// GetOCIStopSignal retrieves the stop signal specified in the OCI image config
|
||||
func GetOCIStopSignal(ctx context.Context, image Image, defaultSignal string) (string, error) {
|
||||
_, err := signal.ParseSignal(defaultSignal)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
ic, err := image.Config(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !images.IsConfigType(ic.MediaType) {
|
||||
return "", fmt.Errorf("unknown image config media type %s", ic.MediaType)
|
||||
}
|
||||
|
||||
var (
|
||||
ociimage v1.Image
|
||||
config v1.ImageConfig
|
||||
)
|
||||
p, err := content.ReadBlob(ctx, image.ContentStore(), ic)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if err = json.Unmarshal(p, &ociimage); err != nil {
|
||||
return "", err
|
||||
}
|
||||
config = ociimage.Config
|
||||
|
||||
if config.StopSignal == "" {
|
||||
return defaultSignal, nil
|
||||
}
|
||||
|
||||
return config.StopSignal, nil
|
||||
}
|
||||
|
||||
// ParseSignal parses a given string into a syscall.Signal
|
||||
// the rawSignal can be a string with "SIG" prefix,
|
||||
// or a signal number in string format.
|
||||
//
|
||||
// Deprecated: Use github.com/moby/sys/signal instead.
|
||||
func ParseSignal(rawSignal string) (syscall.Signal, error) {
|
||||
return signal.ParseSignal(rawSignal)
|
||||
}
|
||||
24
client/snapshotter_default_linux.go
Normal file
24
client/snapshotter_default_linux.go
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
const (
|
||||
// DefaultSnapshotter will set the default snapshotter for the platform.
|
||||
// This will be based on the client compilation target, so take that into
|
||||
// account when choosing this value.
|
||||
DefaultSnapshotter = "overlayfs"
|
||||
)
|
||||
26
client/snapshotter_default_unix.go
Normal file
26
client/snapshotter_default_unix.go
Normal file
@@ -0,0 +1,26 @@
|
||||
//go:build darwin || freebsd || solaris
|
||||
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
const (
|
||||
// DefaultSnapshotter will set the default snapshotter for the platform.
|
||||
// This will be based on the client compilation target, so take that into
|
||||
// account when choosing this value.
|
||||
DefaultSnapshotter = "native"
|
||||
)
|
||||
24
client/snapshotter_default_windows.go
Normal file
24
client/snapshotter_default_windows.go
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
const (
|
||||
// DefaultSnapshotter will set the default snapshotter for the platform.
|
||||
// This will be based on the client compilation target, so take that into
|
||||
// account when choosing this value.
|
||||
DefaultSnapshotter = "windows"
|
||||
)
|
||||
122
client/snapshotter_opts_unix.go
Normal file
122
client/snapshotter_opts_unix.go
Normal file
@@ -0,0 +1,122 @@
|
||||
//go:build !windows
|
||||
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/containerd/containerd/v2/snapshots"
|
||||
)
|
||||
|
||||
const (
|
||||
capaRemapIDs = "remap-ids"
|
||||
capaOnlyRemapIds = "only-remap-ids"
|
||||
)
|
||||
|
||||
// WithRemapperLabels creates the labels used by any supporting snapshotter
|
||||
// to shift the filesystem ownership (user namespace mapping) automatically; currently
|
||||
// supported by the fuse-overlayfs and overlay snapshotters
|
||||
func WithRemapperLabels(ctrUID, hostUID, ctrGID, hostGID, length uint32) snapshots.Opt {
|
||||
return snapshots.WithLabels(map[string]string{
|
||||
snapshots.LabelSnapshotUIDMapping: fmt.Sprintf("%d:%d:%d", ctrUID, hostUID, length),
|
||||
snapshots.LabelSnapshotGIDMapping: fmt.Sprintf("%d:%d:%d", ctrGID, hostGID, length)})
|
||||
}
|
||||
|
||||
func resolveSnapshotOptions(ctx context.Context, client *Client, snapshotterName string, snapshotter snapshots.Snapshotter, parent string, opts ...snapshots.Opt) (string, error) {
|
||||
capabs, err := client.GetSnapshotterCapabilities(ctx, snapshotterName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, capab := range capabs {
|
||||
if capab == capaRemapIDs {
|
||||
// Snapshotter supports ID remapping, we don't need to do anything.
|
||||
return parent, nil
|
||||
}
|
||||
}
|
||||
|
||||
var local snapshots.Info
|
||||
for _, opt := range opts {
|
||||
opt(&local)
|
||||
}
|
||||
|
||||
needsRemap := false
|
||||
var uidMap, gidMap string
|
||||
|
||||
if value, ok := local.Labels[snapshots.LabelSnapshotUIDMapping]; ok {
|
||||
needsRemap = true
|
||||
uidMap = value
|
||||
}
|
||||
if value, ok := local.Labels[snapshots.LabelSnapshotGIDMapping]; ok {
|
||||
needsRemap = true
|
||||
gidMap = value
|
||||
}
|
||||
|
||||
if !needsRemap {
|
||||
return parent, nil
|
||||
}
|
||||
|
||||
capaOnlyRemap := false
|
||||
for _, capa := range capabs {
|
||||
if capa == capaOnlyRemapIds {
|
||||
capaOnlyRemap = true
|
||||
}
|
||||
}
|
||||
|
||||
if capaOnlyRemap {
|
||||
return "", fmt.Errorf("snapshotter %q doesn't support idmap mounts on this host, configure `slow_chown` to allow a slower and expensive fallback", snapshotterName)
|
||||
}
|
||||
|
||||
var ctrUID, hostUID, length uint32
|
||||
_, err = fmt.Sscanf(uidMap, "%d:%d:%d", &ctrUID, &hostUID, &length)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("uidMap unparsable: %w", err)
|
||||
}
|
||||
|
||||
var ctrGID, hostGID, lengthGID uint32
|
||||
_, err = fmt.Sscanf(gidMap, "%d:%d:%d", &ctrGID, &hostGID, &lengthGID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("gidMap unparsable: %w", err)
|
||||
}
|
||||
|
||||
if ctrUID != 0 || ctrGID != 0 {
|
||||
return "", fmt.Errorf("Container UID/GID of 0 only supported currently (%d/%d)", ctrUID, ctrGID)
|
||||
}
|
||||
|
||||
// TODO(dgl): length isn't taken into account for the intermediate snapshot id.
|
||||
usernsID := fmt.Sprintf("%s-%d-%d", parent, hostUID, hostGID)
|
||||
if _, err := snapshotter.Stat(ctx, usernsID); err == nil {
|
||||
return usernsID, nil
|
||||
}
|
||||
mounts, err := snapshotter.Prepare(ctx, usernsID+"-remap", parent)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// TODO(dgl): length isn't taken into account here yet either.
|
||||
if err := remapRootFS(ctx, mounts, hostUID, hostGID); err != nil {
|
||||
snapshotter.Remove(ctx, usernsID+"-remap")
|
||||
return "", err
|
||||
}
|
||||
if err := snapshotter.Commit(ctx, usernsID, usernsID+"-remap"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return usernsID, nil
|
||||
}
|
||||
27
client/snapshotter_opts_windows.go
Normal file
27
client/snapshotter_opts_windows.go
Normal file
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/containerd/containerd/v2/snapshots"
|
||||
)
|
||||
|
||||
func resolveSnapshotOptions(ctx context.Context, client *Client, snapshotterName string, snapshotter snapshots.Snapshotter, parent string, opts ...snapshots.Opt) (string, error) {
|
||||
return parent, nil
|
||||
}
|
||||
700
client/task.go
Normal file
700
client/task.go
Normal file
@@ -0,0 +1,700 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
goruntime "runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/containerd/containerd/v2/api/services/tasks/v1"
|
||||
"github.com/containerd/containerd/v2/api/types"
|
||||
"github.com/containerd/containerd/v2/cio"
|
||||
"github.com/containerd/containerd/v2/content"
|
||||
"github.com/containerd/containerd/v2/diff"
|
||||
"github.com/containerd/containerd/v2/errdefs"
|
||||
"github.com/containerd/containerd/v2/images"
|
||||
"github.com/containerd/containerd/v2/mount"
|
||||
"github.com/containerd/containerd/v2/oci"
|
||||
"github.com/containerd/containerd/v2/plugins"
|
||||
"github.com/containerd/containerd/v2/protobuf"
|
||||
google_protobuf "github.com/containerd/containerd/v2/protobuf/types"
|
||||
"github.com/containerd/containerd/v2/rootfs"
|
||||
"github.com/containerd/containerd/v2/runtime/v2/runc/options"
|
||||
"github.com/containerd/typeurl/v2"
|
||||
digest "github.com/opencontainers/go-digest"
|
||||
is "github.com/opencontainers/image-spec/specs-go"
|
||||
v1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
specs "github.com/opencontainers/runtime-spec/specs-go"
|
||||
)
|
||||
|
||||
// UnknownExitStatus is returned when containerd is unable to
|
||||
// determine the exit status of a process. This can happen if the process never starts
|
||||
// or if an error was encountered when obtaining the exit status, it is set to 255.
|
||||
const UnknownExitStatus = 255
|
||||
|
||||
const (
|
||||
checkpointDateFormat = "01-02-2006-15:04:05"
|
||||
checkpointNameFormat = "containerd.io/checkpoint/%s:%s"
|
||||
)
|
||||
|
||||
// Status returns process status and exit information
|
||||
type Status struct {
|
||||
// Status of the process
|
||||
Status ProcessStatus
|
||||
// ExitStatus returned by the process
|
||||
ExitStatus uint32
|
||||
// ExitedTime is the time at which the process died
|
||||
ExitTime time.Time
|
||||
}
|
||||
|
||||
// ProcessInfo provides platform specific process information
|
||||
type ProcessInfo struct {
|
||||
// Pid is the process ID
|
||||
Pid uint32
|
||||
// Info includes additional process information
|
||||
// Info varies by platform
|
||||
Info *google_protobuf.Any
|
||||
}
|
||||
|
||||
// ProcessStatus returns a human readable status for the Process representing its current status
|
||||
type ProcessStatus string
|
||||
|
||||
const (
|
||||
// Running indicates the process is currently executing
|
||||
Running ProcessStatus = "running"
|
||||
// Created indicates the process has been created within containerd but the
|
||||
// user's defined process has not started
|
||||
Created ProcessStatus = "created"
|
||||
// Stopped indicates that the process has ran and exited
|
||||
Stopped ProcessStatus = "stopped"
|
||||
// Paused indicates that the process is currently paused
|
||||
Paused ProcessStatus = "paused"
|
||||
// Pausing indicates that the process is currently switching from a
|
||||
// running state into a paused state
|
||||
Pausing ProcessStatus = "pausing"
|
||||
// Unknown indicates that we could not determine the status from the runtime
|
||||
Unknown ProcessStatus = "unknown"
|
||||
)
|
||||
|
||||
// IOCloseInfo allows specific io pipes to be closed on a process
|
||||
type IOCloseInfo struct {
|
||||
Stdin bool
|
||||
}
|
||||
|
||||
// IOCloserOpts allows the caller to set specific pipes as closed on a process
|
||||
type IOCloserOpts func(*IOCloseInfo)
|
||||
|
||||
// WithStdinCloser closes the stdin of a process
|
||||
func WithStdinCloser(r *IOCloseInfo) {
|
||||
r.Stdin = true
|
||||
}
|
||||
|
||||
// CheckpointTaskInfo allows specific checkpoint information to be set for the task
|
||||
type CheckpointTaskInfo struct {
|
||||
Name string
|
||||
// ParentCheckpoint is the digest of a parent checkpoint
|
||||
ParentCheckpoint digest.Digest
|
||||
// Options hold runtime specific settings for checkpointing a task
|
||||
Options interface{}
|
||||
|
||||
runtime string
|
||||
}
|
||||
|
||||
// Runtime name for the container
|
||||
func (i *CheckpointTaskInfo) Runtime() string {
|
||||
return i.runtime
|
||||
}
|
||||
|
||||
// CheckpointTaskOpts allows the caller to set checkpoint options
|
||||
type CheckpointTaskOpts func(*CheckpointTaskInfo) error
|
||||
|
||||
// TaskInfo sets options for task creation
|
||||
type TaskInfo struct {
|
||||
// Checkpoint is the Descriptor for an existing checkpoint that can be used
|
||||
// to restore a task's runtime and memory state
|
||||
Checkpoint *types.Descriptor
|
||||
// RootFS is a list of mounts to use as the task's root filesystem
|
||||
RootFS []mount.Mount
|
||||
// Options hold runtime specific settings for task creation
|
||||
Options interface{}
|
||||
// RuntimePath is an absolute path that can be used to overwrite path
|
||||
// to a shim runtime binary.
|
||||
RuntimePath string
|
||||
|
||||
// runtime is the runtime name for the container, and cannot be changed.
|
||||
runtime string
|
||||
}
|
||||
|
||||
// Runtime name for the container
|
||||
func (i *TaskInfo) Runtime() string {
|
||||
return i.runtime
|
||||
}
|
||||
|
||||
// Task is the executable object within containerd
|
||||
type Task interface {
|
||||
Process
|
||||
|
||||
// Pause suspends the execution of the task
|
||||
Pause(context.Context) error
|
||||
// Resume the execution of the task
|
||||
Resume(context.Context) error
|
||||
// Exec creates a new process inside the task
|
||||
Exec(context.Context, string, *specs.Process, cio.Creator) (Process, error)
|
||||
// Pids returns a list of system specific process ids inside the task
|
||||
Pids(context.Context) ([]ProcessInfo, error)
|
||||
// Checkpoint serializes the runtime and memory information of a task into an
|
||||
// OCI Index that can be pushed and pulled from a remote resource.
|
||||
//
|
||||
// Additional software like CRIU maybe required to checkpoint and restore tasks
|
||||
// NOTE: Checkpoint supports to dump task information to a directory, in this way,
|
||||
// an empty OCI Index will be returned.
|
||||
Checkpoint(context.Context, ...CheckpointTaskOpts) (Image, error)
|
||||
// Update modifies executing tasks with updated settings
|
||||
Update(context.Context, ...UpdateTaskOpts) error
|
||||
// LoadProcess loads a previously created exec'd process
|
||||
LoadProcess(context.Context, string, cio.Attach) (Process, error)
|
||||
// Metrics returns task metrics for runtime specific metrics
|
||||
//
|
||||
// The metric types are generic to containerd and change depending on the runtime
|
||||
// For the built in Linux runtime, github.com/containerd/cgroups.Metrics
|
||||
// are returned in protobuf format
|
||||
Metrics(context.Context) (*types.Metric, error)
|
||||
// Spec returns the current OCI specification for the task
|
||||
Spec(context.Context) (*oci.Spec, error)
|
||||
}
|
||||
|
||||
var _ = (Task)(&task{})
|
||||
|
||||
type task struct {
|
||||
client *Client
|
||||
c Container
|
||||
|
||||
io cio.IO
|
||||
id string
|
||||
pid uint32
|
||||
}
|
||||
|
||||
// Spec returns the current OCI specification for the task
|
||||
func (t *task) Spec(ctx context.Context) (*oci.Spec, error) {
|
||||
return t.c.Spec(ctx)
|
||||
}
|
||||
|
||||
// ID of the task
|
||||
func (t *task) ID() string {
|
||||
return t.id
|
||||
}
|
||||
|
||||
// Pid returns the pid or process id for the task
|
||||
func (t *task) Pid() uint32 {
|
||||
return t.pid
|
||||
}
|
||||
|
||||
func (t *task) Start(ctx context.Context) error {
|
||||
r, err := t.client.TaskService().Start(ctx, &tasks.StartRequest{
|
||||
ContainerID: t.id,
|
||||
})
|
||||
if err != nil {
|
||||
if t.io != nil {
|
||||
t.io.Cancel()
|
||||
t.io.Close()
|
||||
}
|
||||
return errdefs.FromGRPC(err)
|
||||
}
|
||||
t.pid = r.Pid
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *task) Kill(ctx context.Context, s syscall.Signal, opts ...KillOpts) error {
|
||||
var i KillInfo
|
||||
for _, o := range opts {
|
||||
if err := o(ctx, &i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err := t.client.TaskService().Kill(ctx, &tasks.KillRequest{
|
||||
Signal: uint32(s),
|
||||
ContainerID: t.id,
|
||||
ExecID: i.ExecID,
|
||||
All: i.All,
|
||||
})
|
||||
if err != nil {
|
||||
return errdefs.FromGRPC(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *task) Pause(ctx context.Context) error {
|
||||
_, err := t.client.TaskService().Pause(ctx, &tasks.PauseTaskRequest{
|
||||
ContainerID: t.id,
|
||||
})
|
||||
return errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
func (t *task) Resume(ctx context.Context) error {
|
||||
_, err := t.client.TaskService().Resume(ctx, &tasks.ResumeTaskRequest{
|
||||
ContainerID: t.id,
|
||||
})
|
||||
return errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
func (t *task) Status(ctx context.Context) (Status, error) {
|
||||
r, err := t.client.TaskService().Get(ctx, &tasks.GetRequest{
|
||||
ContainerID: t.id,
|
||||
})
|
||||
if err != nil {
|
||||
return Status{}, errdefs.FromGRPC(err)
|
||||
}
|
||||
return Status{
|
||||
Status: ProcessStatus(strings.ToLower(r.Process.Status.String())),
|
||||
ExitStatus: r.Process.ExitStatus,
|
||||
ExitTime: protobuf.FromTimestamp(r.Process.ExitedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *task) Wait(ctx context.Context) (<-chan ExitStatus, error) {
|
||||
c := make(chan ExitStatus, 1)
|
||||
go func() {
|
||||
defer close(c)
|
||||
r, err := t.client.TaskService().Wait(ctx, &tasks.WaitRequest{
|
||||
ContainerID: t.id,
|
||||
})
|
||||
if err != nil {
|
||||
c <- ExitStatus{
|
||||
code: UnknownExitStatus,
|
||||
err: err,
|
||||
}
|
||||
return
|
||||
}
|
||||
c <- ExitStatus{
|
||||
code: r.ExitStatus,
|
||||
exitedAt: protobuf.FromTimestamp(r.ExitedAt),
|
||||
}
|
||||
}()
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Delete deletes the task and its runtime state
|
||||
// it returns the exit status of the task and any errors that were encountered
|
||||
// during cleanup
|
||||
func (t *task) Delete(ctx context.Context, opts ...ProcessDeleteOpts) (*ExitStatus, error) {
|
||||
for _, o := range opts {
|
||||
if err := o(ctx, t); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
status, err := t.Status(ctx)
|
||||
if err != nil && errdefs.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
switch status.Status {
|
||||
case Stopped, Unknown, "":
|
||||
case Created:
|
||||
if t.client.runtime == plugins.RuntimePlugin.String()+".windows" {
|
||||
// On windows Created is akin to Stopped
|
||||
break
|
||||
}
|
||||
if t.pid == 0 {
|
||||
// allow for deletion of created tasks with PID 0
|
||||
// https://github.com/containerd/containerd/issues/7357
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
return nil, fmt.Errorf("task must be stopped before deletion: %s: %w", status.Status, errdefs.ErrFailedPrecondition)
|
||||
}
|
||||
if t.io != nil {
|
||||
// io.Wait locks for restored tasks on Windows unless we call
|
||||
// io.Close first (https://github.com/containerd/containerd/issues/5621)
|
||||
// in other cases, preserve the contract and let IO finish before closing
|
||||
if t.client.runtime == plugins.RuntimePlugin.String()+".windows" {
|
||||
t.io.Close()
|
||||
}
|
||||
// io.Cancel is used to cancel the io goroutine while it is in
|
||||
// fifo-opening state. It does not stop the pipes since these
|
||||
// should be closed on the shim's side, otherwise we might lose
|
||||
// data from the container!
|
||||
t.io.Cancel()
|
||||
t.io.Wait()
|
||||
}
|
||||
r, err := t.client.TaskService().Delete(ctx, &tasks.DeleteTaskRequest{
|
||||
ContainerID: t.id,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errdefs.FromGRPC(err)
|
||||
}
|
||||
// Only cleanup the IO after a successful Delete
|
||||
if t.io != nil {
|
||||
t.io.Close()
|
||||
}
|
||||
return &ExitStatus{code: r.ExitStatus, exitedAt: protobuf.FromTimestamp(r.ExitedAt)}, nil
|
||||
}
|
||||
|
||||
func (t *task) Exec(ctx context.Context, id string, spec *specs.Process, ioCreate cio.Creator) (_ Process, err error) {
|
||||
if id == "" {
|
||||
return nil, fmt.Errorf("exec id must not be empty: %w", errdefs.ErrInvalidArgument)
|
||||
}
|
||||
i, err := ioCreate(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil && i != nil {
|
||||
i.Cancel()
|
||||
i.Close()
|
||||
}
|
||||
}()
|
||||
pSpec, err := protobuf.MarshalAnyToProto(spec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg := i.Config()
|
||||
request := &tasks.ExecProcessRequest{
|
||||
ContainerID: t.id,
|
||||
ExecID: id,
|
||||
Terminal: cfg.Terminal,
|
||||
Stdin: cfg.Stdin,
|
||||
Stdout: cfg.Stdout,
|
||||
Stderr: cfg.Stderr,
|
||||
Spec: pSpec,
|
||||
}
|
||||
if _, err := t.client.TaskService().Exec(ctx, request); err != nil {
|
||||
i.Cancel()
|
||||
i.Wait()
|
||||
i.Close()
|
||||
return nil, errdefs.FromGRPC(err)
|
||||
}
|
||||
return &process{
|
||||
id: id,
|
||||
task: t,
|
||||
io: i,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *task) Pids(ctx context.Context) ([]ProcessInfo, error) {
|
||||
response, err := t.client.TaskService().ListPids(ctx, &tasks.ListPidsRequest{
|
||||
ContainerID: t.id,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errdefs.FromGRPC(err)
|
||||
}
|
||||
var processList []ProcessInfo
|
||||
for _, p := range response.Processes {
|
||||
processList = append(processList, ProcessInfo{
|
||||
Pid: p.Pid,
|
||||
Info: p.Info,
|
||||
})
|
||||
}
|
||||
return processList, nil
|
||||
}
|
||||
|
||||
func (t *task) CloseIO(ctx context.Context, opts ...IOCloserOpts) error {
|
||||
r := &tasks.CloseIORequest{
|
||||
ContainerID: t.id,
|
||||
}
|
||||
var i IOCloseInfo
|
||||
for _, o := range opts {
|
||||
o(&i)
|
||||
}
|
||||
r.Stdin = i.Stdin
|
||||
_, err := t.client.TaskService().CloseIO(ctx, r)
|
||||
return errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
func (t *task) IO() cio.IO {
|
||||
return t.io
|
||||
}
|
||||
|
||||
func (t *task) Resize(ctx context.Context, w, h uint32) error {
|
||||
_, err := t.client.TaskService().ResizePty(ctx, &tasks.ResizePtyRequest{
|
||||
ContainerID: t.id,
|
||||
Width: w,
|
||||
Height: h,
|
||||
})
|
||||
return errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
// NOTE: Checkpoint supports to dump task information to a directory, in this way, an empty
|
||||
// OCI Index will be returned.
|
||||
func (t *task) Checkpoint(ctx context.Context, opts ...CheckpointTaskOpts) (Image, error) {
|
||||
ctx, done, err := t.client.WithLease(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer done(ctx)
|
||||
cr, err := t.client.ContainerService().Get(ctx, t.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request := &tasks.CheckpointTaskRequest{
|
||||
ContainerID: t.id,
|
||||
}
|
||||
i := CheckpointTaskInfo{
|
||||
runtime: cr.Runtime.Name,
|
||||
}
|
||||
for _, o := range opts {
|
||||
if err := o(&i); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// set a default name
|
||||
if i.Name == "" {
|
||||
i.Name = fmt.Sprintf(checkpointNameFormat, t.id, time.Now().Format(checkpointDateFormat))
|
||||
}
|
||||
request.ParentCheckpoint = i.ParentCheckpoint.String()
|
||||
if i.Options != nil {
|
||||
o, err := protobuf.MarshalAnyToProto(i.Options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
request.Options = o
|
||||
}
|
||||
|
||||
status, err := t.Status(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if status.Status != Paused {
|
||||
// make sure we pause it and resume after all other filesystem operations are completed
|
||||
if err := t.Pause(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer t.Resume(ctx)
|
||||
}
|
||||
|
||||
index := v1.Index{
|
||||
Versioned: is.Versioned{
|
||||
SchemaVersion: 2,
|
||||
},
|
||||
Annotations: make(map[string]string),
|
||||
}
|
||||
if err := t.checkpointTask(ctx, &index, request); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// if checkpoint image path passed, jump checkpoint image,
|
||||
// return an empty image
|
||||
if isCheckpointPathExist(cr.Runtime.Name, i.Options) {
|
||||
return NewImage(t.client, images.Image{}), nil
|
||||
}
|
||||
|
||||
if cr.Image != "" {
|
||||
if err := t.checkpointImage(ctx, &index, cr.Image); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
index.Annotations["image.name"] = cr.Image
|
||||
}
|
||||
if cr.SnapshotKey != "" {
|
||||
if err := t.checkpointRWSnapshot(ctx, &index, cr.Snapshotter, cr.SnapshotKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
desc, err := writeIndex(ctx, &index, t.client, t.id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
im := images.Image{
|
||||
Name: i.Name,
|
||||
Target: desc,
|
||||
Labels: map[string]string{
|
||||
"containerd.io/checkpoint": "true",
|
||||
},
|
||||
}
|
||||
if im, err = t.client.ImageService().Create(ctx, im); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewImage(t.client, im), nil
|
||||
}
|
||||
|
||||
// UpdateTaskInfo allows updated specific settings to be changed on a task
|
||||
type UpdateTaskInfo struct {
|
||||
// Resources updates a tasks resource constraints
|
||||
Resources interface{}
|
||||
// Annotations allows arbitrary and/or experimental resource constraints for task update
|
||||
Annotations map[string]string
|
||||
}
|
||||
|
||||
// UpdateTaskOpts allows a caller to update task settings
|
||||
type UpdateTaskOpts func(context.Context, *Client, *UpdateTaskInfo) error
|
||||
|
||||
func (t *task) Update(ctx context.Context, opts ...UpdateTaskOpts) error {
|
||||
request := &tasks.UpdateTaskRequest{
|
||||
ContainerID: t.id,
|
||||
}
|
||||
var i UpdateTaskInfo
|
||||
for _, o := range opts {
|
||||
if err := o(ctx, t.client, &i); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if i.Resources != nil {
|
||||
r, err := typeurl.MarshalAny(i.Resources)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
request.Resources = protobuf.FromAny(r)
|
||||
}
|
||||
if i.Annotations != nil {
|
||||
request.Annotations = i.Annotations
|
||||
}
|
||||
_, err := t.client.TaskService().Update(ctx, request)
|
||||
return errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
func (t *task) LoadProcess(ctx context.Context, id string, ioAttach cio.Attach) (Process, error) {
|
||||
if id == t.id && ioAttach == nil {
|
||||
return t, nil
|
||||
}
|
||||
response, err := t.client.TaskService().Get(ctx, &tasks.GetRequest{
|
||||
ContainerID: t.id,
|
||||
ExecID: id,
|
||||
})
|
||||
if err != nil {
|
||||
err = errdefs.FromGRPC(err)
|
||||
if errdefs.IsNotFound(err) {
|
||||
return nil, fmt.Errorf("no running process found: %w", err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
var i cio.IO
|
||||
if ioAttach != nil {
|
||||
if i, err = attachExistingIO(response, ioAttach); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &process{
|
||||
id: id,
|
||||
task: t,
|
||||
io: i,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (t *task) Metrics(ctx context.Context) (*types.Metric, error) {
|
||||
response, err := t.client.TaskService().Metrics(ctx, &tasks.MetricsRequest{
|
||||
Filters: []string{
|
||||
"id==" + t.id,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errdefs.FromGRPC(err)
|
||||
}
|
||||
|
||||
if response.Metrics == nil {
|
||||
_, err := t.Status(ctx)
|
||||
if err != nil && errdefs.IsNotFound(err) {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errors.New("no metrics received")
|
||||
}
|
||||
|
||||
return response.Metrics[0], nil
|
||||
}
|
||||
|
||||
func (t *task) checkpointTask(ctx context.Context, index *v1.Index, request *tasks.CheckpointTaskRequest) error {
|
||||
response, err := t.client.TaskService().Checkpoint(ctx, request)
|
||||
if err != nil {
|
||||
return errdefs.FromGRPC(err)
|
||||
}
|
||||
// NOTE: response.Descriptors can be an empty slice if checkpoint image is jumped
|
||||
// add the checkpoint descriptors to the index
|
||||
for _, d := range response.Descriptors {
|
||||
index.Manifests = append(index.Manifests, v1.Descriptor{
|
||||
MediaType: d.MediaType,
|
||||
Size: d.Size,
|
||||
Digest: digest.Digest(d.Digest),
|
||||
Platform: &v1.Platform{
|
||||
OS: goruntime.GOOS,
|
||||
Architecture: goruntime.GOARCH,
|
||||
},
|
||||
Annotations: d.Annotations,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *task) checkpointRWSnapshot(ctx context.Context, index *v1.Index, snapshotterName string, id string) error {
|
||||
opts := []diff.Opt{
|
||||
diff.WithReference(fmt.Sprintf("checkpoint-rw-%s", id)),
|
||||
}
|
||||
rw, err := rootfs.CreateDiff(ctx, id, t.client.SnapshotService(snapshotterName), t.client.DiffService(), opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rw.Platform = &v1.Platform{
|
||||
OS: goruntime.GOOS,
|
||||
Architecture: goruntime.GOARCH,
|
||||
}
|
||||
index.Manifests = append(index.Manifests, rw)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *task) checkpointImage(ctx context.Context, index *v1.Index, image string) error {
|
||||
if image == "" {
|
||||
return fmt.Errorf("cannot checkpoint image with empty name")
|
||||
}
|
||||
ir, err := t.client.ImageService().Get(ctx, image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
index.Manifests = append(index.Manifests, ir.Target)
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeContent(ctx context.Context, store content.Ingester, mediaType, ref string, r io.Reader, opts ...content.Opt) (d v1.Descriptor, err error) {
|
||||
writer, err := store.Writer(ctx, content.WithRef(ref))
|
||||
if err != nil {
|
||||
return d, err
|
||||
}
|
||||
defer writer.Close()
|
||||
size, err := io.Copy(writer, r)
|
||||
if err != nil {
|
||||
return d, err
|
||||
}
|
||||
|
||||
if err := writer.Commit(ctx, size, "", opts...); err != nil {
|
||||
if !errdefs.IsAlreadyExists(err) {
|
||||
return d, err
|
||||
}
|
||||
}
|
||||
return v1.Descriptor{
|
||||
MediaType: mediaType,
|
||||
Digest: writer.Digest(),
|
||||
Size: size,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// isCheckpointPathExist only suitable for runc runtime now
|
||||
func isCheckpointPathExist(runtime string, v interface{}) bool {
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
switch runtime {
|
||||
case plugins.RuntimeRuncV2:
|
||||
if opts, ok := v.(*options.CheckpointOptions); ok && opts.ImagePath != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
214
client/task_opts.go
Normal file
214
client/task_opts.go
Normal file
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"syscall"
|
||||
|
||||
"github.com/containerd/containerd/v2/api/types"
|
||||
"github.com/containerd/containerd/v2/errdefs"
|
||||
"github.com/containerd/containerd/v2/images"
|
||||
"github.com/containerd/containerd/v2/mount"
|
||||
"github.com/containerd/containerd/v2/runtime/v2/runc/options"
|
||||
"github.com/opencontainers/runtime-spec/specs-go"
|
||||
)
|
||||
|
||||
// NewTaskOpts allows the caller to set options on a new task
|
||||
type NewTaskOpts func(context.Context, *Client, *TaskInfo) error
|
||||
|
||||
// WithRootFS allows a task to be created without a snapshot being allocated to its container
|
||||
func WithRootFS(mounts []mount.Mount) NewTaskOpts {
|
||||
return func(ctx context.Context, c *Client, ti *TaskInfo) error {
|
||||
ti.RootFS = mounts
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithRuntimePath will force task service to use a custom path to the runtime binary
|
||||
// instead of resolving it from runtime name.
|
||||
func WithRuntimePath(absRuntimePath string) NewTaskOpts {
|
||||
return func(ctx context.Context, client *Client, info *TaskInfo) error {
|
||||
info.RuntimePath = absRuntimePath
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithTaskCheckpoint allows a task to be created with live runtime and memory data from a
|
||||
// previous checkpoint. Additional software such as CRIU may be required to
|
||||
// restore a task from a checkpoint
|
||||
func WithTaskCheckpoint(im Image) NewTaskOpts {
|
||||
return func(ctx context.Context, c *Client, info *TaskInfo) error {
|
||||
desc := im.Target()
|
||||
id := desc.Digest
|
||||
index, err := decodeIndex(ctx, c.ContentStore(), desc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, m := range index.Manifests {
|
||||
if m.MediaType == images.MediaTypeContainerd1Checkpoint {
|
||||
info.Checkpoint = &types.Descriptor{
|
||||
MediaType: m.MediaType,
|
||||
Size: m.Size,
|
||||
Digest: m.Digest.String(),
|
||||
Annotations: m.Annotations,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("checkpoint not found in index %s", id)
|
||||
}
|
||||
}
|
||||
|
||||
// WithCheckpointName sets the image name for the checkpoint
|
||||
func WithCheckpointName(name string) CheckpointTaskOpts {
|
||||
return func(r *CheckpointTaskInfo) error {
|
||||
r.Name = name
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithCheckpointImagePath sets image path for checkpoint option
|
||||
func WithCheckpointImagePath(path string) CheckpointTaskOpts {
|
||||
return func(r *CheckpointTaskInfo) error {
|
||||
if r.Options == nil {
|
||||
r.Options = &options.CheckpointOptions{}
|
||||
}
|
||||
opts, ok := r.Options.(*options.CheckpointOptions)
|
||||
if !ok {
|
||||
return errors.New("invalid runtime v2 checkpoint options format")
|
||||
}
|
||||
opts.ImagePath = path
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithRestoreImagePath sets image path for create option
|
||||
func WithRestoreImagePath(path string) NewTaskOpts {
|
||||
return func(ctx context.Context, c *Client, ti *TaskInfo) error {
|
||||
if ti.Options == nil {
|
||||
ti.Options = &options.Options{}
|
||||
}
|
||||
opts, ok := ti.Options.(*options.Options)
|
||||
if !ok {
|
||||
return errors.New("invalid runtime v2 options format")
|
||||
}
|
||||
opts.CriuImagePath = path
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithRestoreWorkPath sets criu work path for create option
|
||||
func WithRestoreWorkPath(path string) NewTaskOpts {
|
||||
return func(ctx context.Context, c *Client, ti *TaskInfo) error {
|
||||
if ti.Options == nil {
|
||||
ti.Options = &options.Options{}
|
||||
}
|
||||
opts, ok := ti.Options.(*options.Options)
|
||||
if !ok {
|
||||
return errors.New("invalid runtime v2 options format")
|
||||
}
|
||||
opts.CriuWorkPath = path
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ProcessDeleteOpts allows the caller to set options for the deletion of a task
|
||||
type ProcessDeleteOpts func(context.Context, Process) error
|
||||
|
||||
// WithProcessKill will forcefully kill and delete a process
|
||||
func WithProcessKill(ctx context.Context, p Process) error {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
// ignore errors to wait and kill as we are forcefully killing
|
||||
// the process and don't care about the exit status
|
||||
s, err := p.Wait(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.Kill(ctx, syscall.SIGKILL, WithKillAll); err != nil {
|
||||
// Kill might still return an IsNotFound error, even if it actually
|
||||
// killed the process.
|
||||
if errdefs.IsNotFound(err) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-s:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if errdefs.IsFailedPrecondition(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
// wait for the process to fully stop before letting the rest of the deletion complete
|
||||
<-s
|
||||
return nil
|
||||
}
|
||||
|
||||
// KillInfo contains information on how to process a Kill action
|
||||
type KillInfo struct {
|
||||
// All kills all processes inside the task
|
||||
// only valid on tasks, ignored on processes
|
||||
All bool
|
||||
// ExecID is the ID of a process to kill
|
||||
ExecID string
|
||||
}
|
||||
|
||||
// KillOpts allows options to be set for the killing of a process
|
||||
type KillOpts func(context.Context, *KillInfo) error
|
||||
|
||||
// WithKillAll kills all processes for a task
|
||||
func WithKillAll(ctx context.Context, i *KillInfo) error {
|
||||
i.All = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithKillExecID specifies the process ID
|
||||
func WithKillExecID(execID string) KillOpts {
|
||||
return func(ctx context.Context, i *KillInfo) error {
|
||||
i.ExecID = execID
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithResources sets the provided resources for task updates. Resources must be
|
||||
// either a *specs.LinuxResources or a *specs.WindowsResources
|
||||
func WithResources(resources interface{}) UpdateTaskOpts {
|
||||
return func(ctx context.Context, client *Client, r *UpdateTaskInfo) error {
|
||||
switch resources.(type) {
|
||||
case *specs.LinuxResources:
|
||||
case *specs.WindowsResources:
|
||||
default:
|
||||
return errors.New("WithResources requires a *specs.LinuxResources or *specs.WindowsResources")
|
||||
}
|
||||
|
||||
r.Resources = resources
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithAnnotations sets the provided annotations for task updates.
|
||||
func WithAnnotations(annotations map[string]string) UpdateTaskOpts {
|
||||
return func(ctx context.Context, client *Client, r *UpdateTaskInfo) error {
|
||||
r.Annotations = annotations
|
||||
return nil
|
||||
}
|
||||
}
|
||||
98
client/task_opts_unix.go
Normal file
98
client/task_opts_unix.go
Normal file
@@ -0,0 +1,98 @@
|
||||
//go:build !windows
|
||||
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/containerd/containerd/v2/runtime/v2/runc/options"
|
||||
)
|
||||
|
||||
// WithNoNewKeyring causes tasks not to be created with a new keyring for secret storage.
|
||||
// There is an upper limit on the number of keyrings in a linux system
|
||||
func WithNoNewKeyring(ctx context.Context, c *Client, ti *TaskInfo) error {
|
||||
if ti.Options == nil {
|
||||
ti.Options = &options.Options{}
|
||||
}
|
||||
opts, ok := ti.Options.(*options.Options)
|
||||
if !ok {
|
||||
return errors.New("invalid v2 shim create options format")
|
||||
}
|
||||
opts.NoNewKeyring = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithNoPivotRoot instructs the runtime not to you pivot_root
|
||||
func WithNoPivotRoot(_ context.Context, _ *Client, ti *TaskInfo) error {
|
||||
if ti.Options == nil {
|
||||
ti.Options = &options.Options{}
|
||||
}
|
||||
opts, ok := ti.Options.(*options.Options)
|
||||
if !ok {
|
||||
return errors.New("invalid v2 shim create options format")
|
||||
}
|
||||
opts.NoPivotRoot = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// WithShimCgroup sets the existing cgroup for the shim
|
||||
func WithShimCgroup(path string) NewTaskOpts {
|
||||
return func(ctx context.Context, c *Client, ti *TaskInfo) error {
|
||||
if ti.Options == nil {
|
||||
ti.Options = &options.Options{}
|
||||
}
|
||||
opts, ok := ti.Options.(*options.Options)
|
||||
if !ok {
|
||||
return errors.New("invalid v2 shim create options format")
|
||||
}
|
||||
opts.ShimCgroup = path
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithUIDOwner allows console I/O to work with the remapped UID in user namespace
|
||||
func WithUIDOwner(uid uint32) NewTaskOpts {
|
||||
return func(ctx context.Context, c *Client, ti *TaskInfo) error {
|
||||
if ti.Options == nil {
|
||||
ti.Options = &options.Options{}
|
||||
}
|
||||
opts, ok := ti.Options.(*options.Options)
|
||||
if !ok {
|
||||
return errors.New("invalid v2 shim create options format")
|
||||
}
|
||||
opts.IoUid = uid
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithGIDOwner allows console I/O to work with the remapped GID in user namespace
|
||||
func WithGIDOwner(gid uint32) NewTaskOpts {
|
||||
return func(ctx context.Context, c *Client, ti *TaskInfo) error {
|
||||
if ti.Options == nil {
|
||||
ti.Options = &options.Options{}
|
||||
}
|
||||
opts, ok := ti.Options.(*options.Options)
|
||||
if !ok {
|
||||
return errors.New("invalid v2 shim create options format")
|
||||
}
|
||||
opts.IoGid = gid
|
||||
return nil
|
||||
}
|
||||
}
|
||||
109
client/transfer.go
Normal file
109
client/transfer.go
Normal file
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
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
|
||||
|
||||
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.
|
||||
*/
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
|
||||
streamingapi "github.com/containerd/containerd/v2/api/services/streaming/v1"
|
||||
transferapi "github.com/containerd/containerd/v2/api/services/transfer/v1"
|
||||
"github.com/containerd/containerd/v2/errdefs"
|
||||
"github.com/containerd/containerd/v2/pkg/streaming"
|
||||
"github.com/containerd/containerd/v2/pkg/transfer"
|
||||
"github.com/containerd/containerd/v2/pkg/transfer/proxy"
|
||||
"github.com/containerd/containerd/v2/protobuf"
|
||||
"github.com/containerd/typeurl/v2"
|
||||
)
|
||||
|
||||
func (c *Client) Transfer(ctx context.Context, src interface{}, dest interface{}, opts ...transfer.Opt) error {
|
||||
ctx, done, err := c.WithLease(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer done(ctx)
|
||||
|
||||
return proxy.NewTransferrer(transferapi.NewTransferClient(c.conn), c.streamCreator()).Transfer(ctx, src, dest, opts...)
|
||||
}
|
||||
|
||||
func (c *Client) streamCreator() streaming.StreamCreator {
|
||||
return &streamCreator{
|
||||
client: streamingapi.NewStreamingClient(c.conn),
|
||||
}
|
||||
}
|
||||
|
||||
type streamCreator struct {
|
||||
client streamingapi.StreamingClient
|
||||
}
|
||||
|
||||
func (sc *streamCreator) Create(ctx context.Context, id string) (streaming.Stream, error) {
|
||||
stream, err := sc.client.Stream(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a, err := typeurl.MarshalAny(&streamingapi.StreamInit{
|
||||
ID: id,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = stream.Send(protobuf.FromAny(a))
|
||||
if err != nil {
|
||||
if !errors.Is(err, io.EOF) {
|
||||
err = errdefs.FromGRPC(err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Receive an ack that stream is init and ready
|
||||
if _, err = stream.Recv(); err != nil {
|
||||
if !errors.Is(err, io.EOF) {
|
||||
err = errdefs.FromGRPC(err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &clientStream{
|
||||
s: stream,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type clientStream struct {
|
||||
s streamingapi.Streaming_StreamClient
|
||||
}
|
||||
|
||||
func (cs *clientStream) Send(a typeurl.Any) (err error) {
|
||||
err = cs.s.Send(protobuf.FromAny(a))
|
||||
if !errors.Is(err, io.EOF) {
|
||||
err = errdefs.FromGRPC(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (cs *clientStream) Recv() (a typeurl.Any, err error) {
|
||||
a, err = cs.s.Recv()
|
||||
if !errors.Is(err, io.EOF) {
|
||||
err = errdefs.FromGRPC(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (cs *clientStream) Close() error {
|
||||
return cs.s.CloseSend()
|
||||
}
|
||||
Reference in New Issue
Block a user