Update containerd to 3c1ef1a714

Signed-off-by: Lantao Liu <lantaol@google.com>
This commit is contained in:
Lantao Liu
2018-03-15 23:21:32 +00:00
parent eff311d493
commit 62d1f13217
84 changed files with 7068 additions and 3748 deletions

View File

@@ -43,7 +43,6 @@ import (
"github.com/containerd/containerd/events"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/namespaces"
"github.com/containerd/containerd/platforms"
"github.com/containerd/containerd/plugin"
"github.com/containerd/containerd/remotes"
"github.com/containerd/containerd/remotes/docker"
@@ -236,6 +235,10 @@ type RemoteContext struct {
// If no resolver is provided, defaults to Docker registry resolver.
Resolver remotes.Resolver
// Platforms defines which platforms to handle when doing the image operation.
// If this field is empty, content for all platforms will be pulled.
Platforms []string
// Unpack is done after an image is pulled to extract into a snapshotter.
// If an image is not unpacked on pull, it can be unpacked any time
// afterwards. Unpacking is required to run an image.
@@ -287,6 +290,7 @@ func (c *Client) Pull(ctx context.Context, ref string, opts ...RemoteOpt) (Image
if err != nil {
return nil, errors.Wrapf(err, "failed to resolve reference %q", ref)
}
fetcher, err := pullCtx.Resolver.Fetcher(ctx, name)
if err != nil {
return nil, errors.Wrapf(err, "failed to get fetcher for %q", name)
@@ -304,8 +308,8 @@ func (c *Client) Pull(ctx context.Context, ref string, opts ...RemoteOpt) (Image
childrenHandler := images.ChildrenHandler(store)
// Set any children labels for that content
childrenHandler = images.SetChildrenLabels(store, childrenHandler)
// Filter the childen by the platform
childrenHandler = images.FilterPlatform(platforms.Default(), childrenHandler)
// Filter childen by platforms
childrenHandler = images.FilterPlatforms(childrenHandler, pullCtx.Platforms...)
handler = images.Handlers(append(pullCtx.BaseHandlers,
remotes.FetchHandler(store, fetcher),
@@ -371,7 +375,7 @@ func (c *Client) Push(ctx context.Context, ref string, desc ocispec.Descriptor,
return err
}
return remotes.PushContent(ctx, pusher, desc, c.ContentStore(), pushCtx.BaseHandlers...)
return remotes.PushContent(ctx, pusher, desc, c.ContentStore(), pushCtx.Platforms, pushCtx.BaseHandlers...)
}
// GetImage returns an existing image

View File

@@ -64,6 +64,21 @@ func WithServices(opts ...ServicesOpt) ClientOpt {
// 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 {
return func(_ *Client, c *RemoteContext) error {
for _, p := range c.Platforms {
if p == platform {
return nil
}
}
c.Platforms = append(c.Platforms, platform)
return nil
}
}
// WithPullUnpack is used to unpack an image after pull. This
// uses the snapshotter, content store, and diff service
// configured for the client.

View File

@@ -0,0 +1,113 @@
/*
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 command
import (
"fmt"
"io/ioutil"
"log"
"github.com/containerd/containerd/cmd/ctr/commands/containers"
"github.com/containerd/containerd/cmd/ctr/commands/content"
"github.com/containerd/containerd/cmd/ctr/commands/events"
"github.com/containerd/containerd/cmd/ctr/commands/images"
namespacesCmd "github.com/containerd/containerd/cmd/ctr/commands/namespaces"
"github.com/containerd/containerd/cmd/ctr/commands/plugins"
"github.com/containerd/containerd/cmd/ctr/commands/pprof"
"github.com/containerd/containerd/cmd/ctr/commands/run"
"github.com/containerd/containerd/cmd/ctr/commands/snapshots"
"github.com/containerd/containerd/cmd/ctr/commands/tasks"
versionCmd "github.com/containerd/containerd/cmd/ctr/commands/version"
"github.com/containerd/containerd/defaults"
"github.com/containerd/containerd/namespaces"
"github.com/containerd/containerd/version"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"google.golang.org/grpc/grpclog"
)
var extraCmds = []cli.Command{}
func init() {
// Discard grpc logs so that they don't mess with our stdio
grpclog.SetLogger(log.New(ioutil.Discard, "", log.LstdFlags))
cli.VersionPrinter = func(c *cli.Context) {
fmt.Println(c.App.Name, version.Package, c.App.Version)
}
}
// App returns a *cli.App instance.
func App() *cli.App {
app := cli.NewApp()
app.Name = "ctr"
app.Version = version.Version
app.Usage = `
__
_____/ /______
/ ___/ __/ ___/
/ /__/ /_/ /
\___/\__/_/
containerd CLI
`
app.Flags = []cli.Flag{
cli.BoolFlag{
Name: "debug",
Usage: "enable debug output in logs",
},
cli.StringFlag{
Name: "address, a",
Usage: "address for containerd's GRPC server",
Value: defaults.DefaultAddress,
},
cli.DurationFlag{
Name: "timeout",
Usage: "total timeout for ctr commands",
},
cli.DurationFlag{
Name: "connect-timeout",
Usage: "timeout for connecting to containerd",
},
cli.StringFlag{
Name: "namespace, n",
Usage: "namespace to use with commands",
Value: namespaces.Default,
EnvVar: namespaces.NamespaceEnvVar,
},
}
app.Commands = append([]cli.Command{
plugins.Command,
versionCmd.Command,
containers.Command,
content.Command,
events.Command,
images.Command,
namespacesCmd.Command,
pprof.Command,
run.Command,
snapshots.Command,
tasks.Command,
}, extraCmds...)
app.Before = func(context *cli.Context) error {
if context.GlobalBool("debug") {
logrus.SetLevel(logrus.DebugLevel)
}
return nil
}
return app
}

View File

@@ -0,0 +1,25 @@
// +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 command
import "github.com/containerd/containerd/cmd/ctr/commands/shim"
func init() {
extraCmds = append(extraCmds, shim.Command)
}

View File

@@ -0,0 +1,56 @@
/*
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 commands
import (
gocontext "context"
"github.com/containerd/containerd"
"github.com/containerd/containerd/namespaces"
"github.com/urfave/cli"
)
// AppContext returns the context for a command. Should only be called once per
// command, near the start.
//
// This will ensure the namespace is picked up and set the timeout, if one is
// defined.
func AppContext(context *cli.Context) (gocontext.Context, gocontext.CancelFunc) {
var (
ctx = gocontext.Background()
timeout = context.GlobalDuration("timeout")
namespace = context.GlobalString("namespace")
cancel gocontext.CancelFunc
)
ctx = namespaces.WithNamespace(ctx, namespace)
if timeout > 0 {
ctx, cancel = gocontext.WithTimeout(ctx, timeout)
} else {
ctx, cancel = gocontext.WithCancel(ctx)
}
return ctx, cancel
}
// NewClient returns a new containerd client
func NewClient(context *cli.Context) (*containerd.Client, gocontext.Context, gocontext.CancelFunc, error) {
client, err := containerd.New(context.GlobalString("address"))
if err != nil {
return nil, nil, nil, err
}
ctx, cancel := AppContext(context)
return client, ctx, cancel, nil
}

View 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 commands
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/containerd/containerd"
"github.com/urfave/cli"
)
var (
// SnapshotterFlags are cli flags specifying snapshotter names
SnapshotterFlags = []cli.Flag{
cli.StringFlag{
Name: "snapshotter",
Usage: "snapshotter name. Empty value stands for the default value.",
Value: containerd.DefaultSnapshotter,
EnvVar: "CONTAINERD_SNAPSHOTTER",
},
}
// LabelFlag is a cli flag specifying labels
LabelFlag = cli.StringSliceFlag{
Name: "label",
Usage: "labels to attach to the image",
}
// RegistryFlags are cli flags specifying registry options
RegistryFlags = []cli.Flag{
cli.BoolFlag{
Name: "skip-verify,k",
Usage: "skip SSL certificate validation",
},
cli.BoolFlag{
Name: "plain-http",
Usage: "allow connections using plain HTTP",
},
cli.StringFlag{
Name: "user,u",
Usage: "user[:password] Registry user and password",
},
cli.StringFlag{
Name: "refresh",
Usage: "refresh token for authorization server",
},
}
)
// ObjectWithLabelArgs returns the first arg and a LabelArgs object
func ObjectWithLabelArgs(clicontext *cli.Context) (string, map[string]string) {
var (
first = clicontext.Args().First()
labelStrings = clicontext.Args().Tail()
)
return first, LabelArgs(labelStrings)
}
// LabelArgs returns a map of label key,value pairs
func LabelArgs(labelStrings []string) map[string]string {
labels := make(map[string]string, len(labelStrings))
for _, label := range labelStrings {
parts := strings.SplitN(label, "=", 2)
key := parts[0]
value := "true"
if len(parts) > 1 {
value = parts[1]
}
labels[key] = value
}
return labels
}
// PrintAsJSON prints input in JSON format
func PrintAsJSON(x interface{}) {
b, err := json.MarshalIndent(x, "", " ")
if err != nil {
fmt.Fprintf(os.Stderr, "can't marshal %+v as a JSON string: %v\n", x, err)
}
fmt.Println(string(b))
}
// WritePidFile writes the pid atomically to a file
func WritePidFile(path string, pid int) error {
path, err := filepath.Abs(path)
if err != nil {
return err
}
tempPath := filepath.Join(filepath.Dir(path), fmt.Sprintf(".%s", filepath.Base(path)))
f, err := os.OpenFile(tempPath, os.O_RDWR|os.O_CREATE|os.O_EXCL|os.O_SYNC, 0666)
if err != nil {
return err
}
_, err = fmt.Fprintf(f, "%d", pid)
f.Close()
if err != nil {
return err
}
return os.Rename(tempPath, path)
}

View File

@@ -0,0 +1,257 @@
/*
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 containers
import (
"context"
"errors"
"fmt"
"os"
"strings"
"text/tabwriter"
"github.com/containerd/containerd"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/cmd/ctr/commands/run"
"github.com/containerd/containerd/log"
"github.com/urfave/cli"
)
// Command is the cli command for managing containers
var Command = cli.Command{
Name: "containers",
Usage: "manage containers",
Aliases: []string{"c", "container"},
Subcommands: []cli.Command{
createCommand,
deleteCommand,
infoCommand,
listCommand,
setLabelsCommand,
},
}
var createCommand = cli.Command{
Name: "create",
Usage: "create container",
ArgsUsage: "[flags] Image|RootFS CONTAINER",
Flags: append(commands.SnapshotterFlags, run.ContainerFlags...),
Action: func(context *cli.Context) error {
var (
id = context.Args().Get(1)
ref = context.Args().First()
)
if ref == "" {
return errors.New("image ref must be provided")
}
if id == "" {
return errors.New("container id must be provided")
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
_, err = run.NewContainer(ctx, client, context)
if err != nil {
return err
}
return nil
},
}
var listCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "list containers",
ArgsUsage: "[flags] [<filter>, ...]",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "quiet, q",
Usage: "print only the container id",
},
},
Action: func(context *cli.Context) error {
var (
filters = context.Args()
quiet = context.Bool("quiet")
)
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
containers, err := client.Containers(ctx, filters...)
if err != nil {
return err
}
if quiet {
for _, c := range containers {
fmt.Printf("%s\n", c.ID())
}
return nil
}
w := tabwriter.NewWriter(os.Stdout, 4, 8, 4, ' ', 0)
fmt.Fprintln(w, "CONTAINER\tIMAGE\tRUNTIME\t")
for _, c := range containers {
info, err := c.Info(ctx)
if err != nil {
return err
}
imageName := info.Image
if imageName == "" {
imageName = "-"
}
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t\n",
c.ID(),
imageName,
info.Runtime.Name,
); err != nil {
return err
}
}
return w.Flush()
},
}
var deleteCommand = cli.Command{
Name: "delete",
Usage: "delete one or more existing containers",
ArgsUsage: "[flags] CONTAINER [CONTAINER, ...]",
Aliases: []string{"del", "rm"},
Flags: []cli.Flag{
cli.BoolFlag{
Name: "keep-snapshot",
Usage: "do not clean up snapshot with container",
},
},
Action: func(context *cli.Context) error {
var exitErr error
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
deleteOpts := []containerd.DeleteOpts{}
if !context.Bool("keep-snapshot") {
deleteOpts = append(deleteOpts, containerd.WithSnapshotCleanup)
}
if context.NArg() == 0 {
return errors.New("must specify at least one container to delete")
}
for _, arg := range context.Args() {
if err := deleteContainer(ctx, client, arg, deleteOpts...); err != nil {
if exitErr == nil {
exitErr = err
}
log.G(ctx).WithError(err).Errorf("failed to delete container %q", arg)
}
}
return exitErr
},
}
func deleteContainer(ctx context.Context, client *containerd.Client, id string, opts ...containerd.DeleteOpts) error {
container, err := client.LoadContainer(ctx, id)
if err != nil {
return err
}
task, err := container.Task(ctx, nil)
if err != nil {
return container.Delete(ctx, opts...)
}
status, err := task.Status(ctx)
if err != nil {
return err
}
if status.Status == containerd.Stopped || status.Status == containerd.Created {
if _, err := task.Delete(ctx); err != nil {
return err
}
return container.Delete(ctx, opts...)
}
return fmt.Errorf("cannot delete a non stopped container: %v", status)
}
var setLabelsCommand = cli.Command{
Name: "label",
Usage: "set and clear labels for a container",
ArgsUsage: "[flags] CONTAINER [<key>=<value>, ...]",
Description: "set and clear labels for a container",
Flags: []cli.Flag{},
Action: func(context *cli.Context) error {
containerID, labels := commands.ObjectWithLabelArgs(context)
if containerID == "" {
return errors.New("container id must be provided")
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
container, err := client.LoadContainer(ctx, containerID)
if err != nil {
return err
}
setlabels, err := container.SetLabels(ctx, labels)
if err != nil {
return err
}
var labelStrings []string
for k, v := range setlabels {
labelStrings = append(labelStrings, fmt.Sprintf("%s=%s", k, v))
}
fmt.Println(strings.Join(labelStrings, ","))
return nil
},
}
var infoCommand = cli.Command{
Name: "info",
Usage: "get info about a container",
ArgsUsage: "CONTAINER",
Action: func(context *cli.Context) error {
id := context.Args().First()
if id == "" {
return errors.New("container id must be provided")
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
container, err := client.LoadContainer(ctx, id)
if err != nil {
return err
}
info, err := container.Info(ctx)
if err != nil {
return err
}
commands.PrintAsJSON(info)
return nil
},
}

View File

@@ -0,0 +1,560 @@
/*
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 content
import (
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"strings"
"text/tabwriter"
"time"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/log"
units "github.com/docker/go-units"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/urfave/cli"
)
var (
// Command is the cli command for managing content
Command = cli.Command{
Name: "content",
Usage: "manage content",
Subcommands: cli.Commands{
activeIngestCommand,
deleteCommand,
editCommand,
fetchCommand,
fetchObjectCommand,
getCommand,
ingestCommand,
listCommand,
pushObjectCommand,
setLabelsCommand,
},
}
getCommand = cli.Command{
Name: "get",
Usage: "get the data for an object",
ArgsUsage: "[<digest>, ...]",
Description: "display the image object",
Action: func(context *cli.Context) error {
dgst, err := digest.Parse(context.Args().First())
if err != nil {
return err
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
cs := client.ContentStore()
ra, err := cs.ReaderAt(ctx, dgst)
if err != nil {
return err
}
defer ra.Close()
_, err = io.Copy(os.Stdout, content.NewReader(ra))
return err
},
}
ingestCommand = cli.Command{
Name: "ingest",
Usage: "accept content into the store",
ArgsUsage: "[flags] <key>",
Description: "ingest objects into the local content store",
Flags: []cli.Flag{
cli.Int64Flag{
Name: "expected-size",
Usage: "validate against provided size",
},
cli.StringFlag{
Name: "expected-digest",
Usage: "verify content against expected digest",
},
},
Action: func(context *cli.Context) error {
var (
ref = context.Args().First()
expectedSize = context.Int64("expected-size")
expectedDigest = digest.Digest(context.String("expected-digest"))
)
if err := expectedDigest.Validate(); expectedDigest != "" && err != nil {
return err
}
if ref == "" {
return errors.New("must specify a transaction reference")
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
cs := client.ContentStore()
// TODO(stevvooe): Allow ingest to be reentrant. Currently, we expect
// all data to be written in a single invocation. Allow multiple writes
// to the same transaction key followed by a commit.
return content.WriteBlob(ctx, cs, ref, os.Stdin, expectedSize, expectedDigest)
},
}
activeIngestCommand = cli.Command{
Name: "active",
Usage: "display active transfers",
ArgsUsage: "[flags] [<regexp>]",
Description: "display the ongoing transfers",
Flags: []cli.Flag{
cli.DurationFlag{
Name: "timeout, t",
Usage: "total timeout for fetch",
EnvVar: "CONTAINERD_FETCH_TIMEOUT",
},
cli.StringFlag{
Name: "root",
Usage: "path to content store root",
Value: "/tmp/content", // TODO(stevvooe): for now, just use the PWD/.content
},
},
Action: func(context *cli.Context) error {
match := context.Args().First()
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
cs := client.ContentStore()
active, err := cs.ListStatuses(ctx, match)
if err != nil {
return err
}
tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, '\t', 0)
fmt.Fprintln(tw, "REF\tSIZE\tAGE\t")
for _, active := range active {
fmt.Fprintf(tw, "%s\t%s\t%s\t\n",
active.Ref,
units.HumanSize(float64(active.Offset)),
units.HumanDuration(time.Since(active.StartedAt)))
}
return tw.Flush()
},
}
listCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "list all blobs in the store",
ArgsUsage: "[flags]",
Description: "list blobs in the content store",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "quiet, q",
Usage: "print only the blob digest",
},
},
Action: func(context *cli.Context) error {
var (
quiet = context.Bool("quiet")
args = []string(context.Args())
)
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
cs := client.ContentStore()
var walkFn content.WalkFunc
if quiet {
walkFn = func(info content.Info) error {
fmt.Println(info.Digest)
return nil
}
} else {
tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, '\t', 0)
defer tw.Flush()
fmt.Fprintln(tw, "DIGEST\tSIZE\tAGE\tLABELS")
walkFn = func(info content.Info) error {
var labelStrings []string
for k, v := range info.Labels {
labelStrings = append(labelStrings, strings.Join([]string{k, v}, "="))
}
labels := strings.Join(labelStrings, ",")
if labels == "" {
labels = "-"
}
fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n",
info.Digest,
units.HumanSize(float64(info.Size)),
units.HumanDuration(time.Since(info.CreatedAt)),
labels)
return nil
}
}
return cs.Walk(ctx, walkFn, args...)
},
}
setLabelsCommand = cli.Command{
Name: "label",
Usage: "add labels to content",
ArgsUsage: "<digest> [<label>=<value> ...]",
Description: "labels blobs in the content store",
Action: func(context *cli.Context) error {
object, labels := commands.ObjectWithLabelArgs(context)
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
cs := client.ContentStore()
dgst, err := digest.Parse(object)
if err != nil {
return err
}
info := content.Info{
Digest: dgst,
Labels: map[string]string{},
}
var paths []string
for k, v := range labels {
paths = append(paths, fmt.Sprintf("labels.%s", k))
if v != "" {
info.Labels[k] = v
}
}
// Nothing updated, do no clear
if len(paths) == 0 {
info, err = cs.Info(ctx, info.Digest)
} else {
info, err = cs.Update(ctx, info, paths...)
}
if err != nil {
return err
}
var labelStrings []string
for k, v := range info.Labels {
labelStrings = append(labelStrings, fmt.Sprintf("%s=%s", k, v))
}
fmt.Println(strings.Join(labelStrings, ","))
return nil
},
}
editCommand = cli.Command{
Name: "edit",
Usage: "edit a blob and return a new digest",
ArgsUsage: "[flags] <digest>",
Description: "edit a blob and return a new digest",
Flags: []cli.Flag{
cli.StringFlag{
Name: "validate",
Usage: "validate the result against a format (json, mediatype, etc.)",
},
},
Action: func(context *cli.Context) error {
var (
validate = context.String("validate")
object = context.Args().First()
)
if validate != "" {
return errors.New("validating the edit result not supported")
}
// TODO(stevvooe): Support looking up objects by a reference through
// the image metadata storage.
dgst, err := digest.Parse(object)
if err != nil {
return err
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
cs := client.ContentStore()
ra, err := cs.ReaderAt(ctx, dgst)
if err != nil {
return err
}
defer ra.Close()
nrc, err := edit(content.NewReader(ra))
if err != nil {
return err
}
defer nrc.Close()
wr, err := cs.Writer(ctx, "edit-"+object, 0, "") // TODO(stevvooe): Choose a better key?
if err != nil {
return err
}
if _, err := io.Copy(wr, nrc); err != nil {
return err
}
if err := wr.Commit(ctx, 0, wr.Digest()); err != nil {
return err
}
fmt.Println(wr.Digest())
return nil
},
}
deleteCommand = cli.Command{
Name: "delete",
Aliases: []string{"del", "remove", "rm"},
Usage: "permanently delete one or more blobs",
ArgsUsage: "[<digest>, ...]",
Description: `Delete one or more blobs permanently. Successfully deleted
blobs are printed to stdout.`,
Action: func(context *cli.Context) error {
var (
args = []string(context.Args())
exitError error
)
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
cs := client.ContentStore()
for _, arg := range args {
dgst, err := digest.Parse(arg)
if err != nil {
if exitError == nil {
exitError = err
}
log.G(ctx).WithError(err).Errorf("could not delete %v", dgst)
continue
}
if err := cs.Delete(ctx, dgst); err != nil {
if !errdefs.IsNotFound(err) {
if exitError == nil {
exitError = err
}
log.G(ctx).WithError(err).Errorf("could not delete %v", dgst)
}
continue
}
fmt.Println(dgst)
}
return exitError
},
}
// TODO(stevvooe): Create "multi-fetch" mode that just takes a remote
// then receives object/hint lines on stdin, returning content as
// needed.
fetchObjectCommand = cli.Command{
Name: "fetch-object",
Usage: "retrieve objects from a remote",
ArgsUsage: "[flags] <remote> <object> [<hint>, ...]",
Description: `Fetch objects by identifier from a remote.`,
Flags: commands.RegistryFlags,
Action: func(context *cli.Context) error {
var (
ref = context.Args().First()
)
ctx, cancel := commands.AppContext(context)
defer cancel()
resolver, err := commands.GetResolver(ctx, context)
if err != nil {
return err
}
ctx = log.WithLogger(ctx, log.G(ctx).WithField("ref", ref))
log.G(ctx).Debugf("resolving")
name, desc, err := resolver.Resolve(ctx, ref)
if err != nil {
return err
}
fetcher, err := resolver.Fetcher(ctx, name)
if err != nil {
return err
}
log.G(ctx).Debugf("fetching")
rc, err := fetcher.Fetch(ctx, desc)
if err != nil {
return err
}
defer rc.Close()
_, err = io.Copy(os.Stdout, rc)
return err
},
}
pushObjectCommand = cli.Command{
Name: "push-object",
Usage: "push an object to a remote",
ArgsUsage: "[flags] <remote> <object> <type>",
Description: `Push objects by identifier to a remote.`,
Flags: commands.RegistryFlags,
Action: func(context *cli.Context) error {
var (
ref = context.Args().Get(0)
object = context.Args().Get(1)
media = context.Args().Get(2)
)
dgst, err := digest.Parse(object)
if err != nil {
return err
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
resolver, err := commands.GetResolver(ctx, context)
if err != nil {
return err
}
ctx = log.WithLogger(ctx, log.G(ctx).WithField("ref", ref))
log.G(ctx).Debugf("resolving")
pusher, err := resolver.Pusher(ctx, ref)
if err != nil {
return err
}
cs := client.ContentStore()
info, err := cs.Info(ctx, dgst)
if err != nil {
return err
}
desc := ocispec.Descriptor{
MediaType: media,
Digest: dgst,
Size: info.Size,
}
ra, err := cs.ReaderAt(ctx, dgst)
if err != nil {
return err
}
defer ra.Close()
cw, err := pusher.Push(ctx, desc)
if err != nil {
return err
}
// TODO: Progress reader
if err := content.Copy(ctx, cw, content.NewReader(ra), desc.Size, desc.Digest); err != nil {
return err
}
fmt.Printf("Pushed %s %s\n", desc.Digest, desc.MediaType)
return nil
},
}
)
func edit(rd io.Reader) (io.ReadCloser, error) {
tmp, err := ioutil.TempFile("", "edit-")
if err != nil {
return nil, err
}
if _, err := io.Copy(tmp, rd); err != nil {
tmp.Close()
return nil, err
}
cmd := exec.Command("sh", "-c", "$EDITOR "+tmp.Name())
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()
if err := cmd.Run(); err != nil {
tmp.Close()
return nil, err
}
if _, err := tmp.Seek(0, io.SeekStart); err != nil {
tmp.Close()
return nil, err
}
return onCloser{ReadCloser: tmp, onClose: func() error {
return os.RemoveAll(tmp.Name())
}}, nil
}
type onCloser struct {
io.ReadCloser
onClose func() error
}
func (oc onCloser) Close() error {
var err error
if err1 := oc.ReadCloser.Close(); err1 != nil {
err = err1
}
if oc.onClose != nil {
err1 := oc.onClose()
if err == nil {
err = err1
}
}
return err
}

View File

@@ -0,0 +1,340 @@
/*
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 content
import (
"context"
"fmt"
"io"
"os"
"sync"
"text/tabwriter"
"time"
"github.com/containerd/containerd"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/progress"
"github.com/containerd/containerd/remotes"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/urfave/cli"
)
var fetchCommand = cli.Command{
Name: "fetch",
Usage: "fetch all content for an image into containerd",
ArgsUsage: "[flags] <remote> <object>",
Description: `Fetch an image into containerd.
This command ensures that containerd has all the necessary resources to build
an image's rootfs and convert the configuration to a runtime format supported
by containerd.
This command uses the same syntax, of remote and object, as 'ctr fetch-object'.
We may want to make this nicer, but agnostism is preferred for the moment.
Right now, the responsibility of the daemon and the cli aren't quite clear. Do
not use this implementation as a guide. The end goal should be having metadata,
content and snapshots ready for a direct use via the 'ctr run'.
Most of this is experimental and there are few leaps to make this work.`,
Flags: append(commands.RegistryFlags, commands.LabelFlag),
Action: func(clicontext *cli.Context) error {
var (
ref = clicontext.Args().First()
)
_, err := Fetch(ref, clicontext)
return err
},
}
// Fetch loads all resources into the content store and returns the image
func Fetch(ref string, cliContext *cli.Context) (containerd.Image, error) {
client, ctx, cancel, err := commands.NewClient(cliContext)
if err != nil {
return nil, err
}
defer cancel()
resolver, err := commands.GetResolver(ctx, cliContext)
if err != nil {
return nil, err
}
ongoing := newJobs(ref)
pctx, stopProgress := context.WithCancel(ctx)
progress := make(chan struct{})
go func() {
if !cliContext.GlobalBool("debug") {
// no progress bar, because it hides some debug logs
showProgress(pctx, ongoing, client.ContentStore(), os.Stdout)
}
close(progress)
}()
h := images.HandlerFunc(func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
if desc.MediaType != images.MediaTypeDockerSchema1Manifest {
ongoing.add(desc)
}
return nil, nil
})
log.G(pctx).WithField("image", ref).Debug("fetching")
labels := commands.LabelArgs(cliContext.StringSlice("label"))
opts := []containerd.RemoteOpt{
containerd.WithPullLabels(labels),
containerd.WithResolver(resolver),
containerd.WithImageHandler(h),
containerd.WithSchema1Conversion,
}
if !cliContext.Bool("all-platforms") {
for _, platform := range cliContext.StringSlice("platform") {
opts = append(opts, containerd.WithPlatform(platform))
}
}
img, err := client.Pull(pctx, ref, opts...)
stopProgress()
if err != nil {
return nil, err
}
<-progress
return img, nil
}
func showProgress(ctx context.Context, ongoing *jobs, cs content.Store, out io.Writer) {
var (
ticker = time.NewTicker(100 * time.Millisecond)
fw = progress.NewWriter(out)
start = time.Now()
statuses = map[string]StatusInfo{}
done bool
)
defer ticker.Stop()
outer:
for {
select {
case <-ticker.C:
fw.Flush()
tw := tabwriter.NewWriter(fw, 1, 8, 1, ' ', 0)
resolved := "resolved"
if !ongoing.isResolved() {
resolved = "resolving"
}
statuses[ongoing.name] = StatusInfo{
Ref: ongoing.name,
Status: resolved,
}
keys := []string{ongoing.name}
activeSeen := map[string]struct{}{}
if !done {
active, err := cs.ListStatuses(ctx, "")
if err != nil {
log.G(ctx).WithError(err).Error("active check failed")
continue
}
// update status of active entries!
for _, active := range active {
statuses[active.Ref] = StatusInfo{
Ref: active.Ref,
Status: "downloading",
Offset: active.Offset,
Total: active.Total,
StartedAt: active.StartedAt,
UpdatedAt: active.UpdatedAt,
}
activeSeen[active.Ref] = struct{}{}
}
}
// now, update the items in jobs that are not in active
for _, j := range ongoing.jobs() {
key := remotes.MakeRefKey(ctx, j)
keys = append(keys, key)
if _, ok := activeSeen[key]; ok {
continue
}
status, ok := statuses[key]
if !done && (!ok || status.Status == "downloading") {
info, err := cs.Info(ctx, j.Digest)
if err != nil {
if !errdefs.IsNotFound(err) {
log.G(ctx).WithError(err).Errorf("failed to get content info")
continue outer
} else {
statuses[key] = StatusInfo{
Ref: key,
Status: "waiting",
}
}
} else if info.CreatedAt.After(start) {
statuses[key] = StatusInfo{
Ref: key,
Status: "done",
Offset: info.Size,
Total: info.Size,
UpdatedAt: info.CreatedAt,
}
} else {
statuses[key] = StatusInfo{
Ref: key,
Status: "exists",
}
}
} else if done {
if ok {
if status.Status != "done" && status.Status != "exists" {
status.Status = "done"
statuses[key] = status
}
} else {
statuses[key] = StatusInfo{
Ref: key,
Status: "done",
}
}
}
}
var ordered []StatusInfo
for _, key := range keys {
ordered = append(ordered, statuses[key])
}
Display(tw, ordered, start)
tw.Flush()
if done {
fw.Flush()
return
}
case <-ctx.Done():
done = true // allow ui to update once more
}
}
}
// jobs provides a way of identifying the download keys for a particular task
// encountering during the pull walk.
//
// This is very minimal and will probably be replaced with something more
// featured.
type jobs struct {
name string
added map[digest.Digest]struct{}
descs []ocispec.Descriptor
mu sync.Mutex
resolved bool
}
func newJobs(name string) *jobs {
return &jobs{
name: name,
added: map[digest.Digest]struct{}{},
}
}
func (j *jobs) add(desc ocispec.Descriptor) {
j.mu.Lock()
defer j.mu.Unlock()
j.resolved = true
if _, ok := j.added[desc.Digest]; ok {
return
}
j.descs = append(j.descs, desc)
j.added[desc.Digest] = struct{}{}
}
func (j *jobs) jobs() []ocispec.Descriptor {
j.mu.Lock()
defer j.mu.Unlock()
var descs []ocispec.Descriptor
return append(descs, j.descs...)
}
func (j *jobs) isResolved() bool {
j.mu.Lock()
defer j.mu.Unlock()
return j.resolved
}
// StatusInfo holds the status info for an upload or download
type StatusInfo struct {
Ref string
Status string
Offset int64
Total int64
StartedAt time.Time
UpdatedAt time.Time
}
// Display pretty prints out the download or upload progress
func Display(w io.Writer, statuses []StatusInfo, start time.Time) {
var total int64
for _, status := range statuses {
total += status.Offset
switch status.Status {
case "downloading", "uploading":
var bar progress.Bar
if status.Total > 0.0 {
bar = progress.Bar(float64(status.Offset) / float64(status.Total))
}
fmt.Fprintf(w, "%s:\t%s\t%40r\t%8.8s/%s\t\n",
status.Ref,
status.Status,
bar,
progress.Bytes(status.Offset), progress.Bytes(status.Total))
case "resolving", "waiting":
bar := progress.Bar(0.0)
fmt.Fprintf(w, "%s:\t%s\t%40r\t\n",
status.Ref,
status.Status,
bar)
default:
bar := progress.Bar(1.0)
fmt.Fprintf(w, "%s:\t%s\t%40r\t\n",
status.Ref,
status.Status,
bar)
}
}
fmt.Fprintf(w, "elapsed: %-4.1fs\ttotal: %7.6v\t(%v)\t\n",
time.Since(start).Seconds(),
// TODO(stevvooe): These calculations are actually way off.
// Need to account for previously downloaded data. These
// will basically be right for a download the first time
// but will be skewed if restarting, as it includes the
// data into the start time before.
progress.Bytes(total),
progress.NewBytesPerSecond(total, time.Since(start)))
}

View File

@@ -0,0 +1,79 @@
/*
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 events
import (
"encoding/json"
"fmt"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/events"
"github.com/containerd/typeurl"
"github.com/urfave/cli"
// Register grpc event types
_ "github.com/containerd/containerd/api/events"
)
// Command is the cli command for displaying containerd events
var Command = cli.Command{
Name: "events",
Aliases: []string{"event"},
Usage: "display containerd events",
Action: func(context *cli.Context) error {
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
eventsClient := client.EventService()
eventsCh, errCh := eventsClient.Subscribe(ctx, context.Args()...)
for {
var e *events.Envelope
select {
case evt, closed := <-eventsCh:
if closed {
return nil
}
e = evt
case err := <-errCh:
return err
}
var out []byte
if e.Event != nil {
v, err := typeurl.UnmarshalAny(e.Event)
if err != nil {
return err
}
out, err = json.Marshal(v)
if err != nil {
return err
}
}
if _, err := fmt.Println(
e.Timestamp,
e.Namespace,
e.Topic,
string(out),
); err != nil {
return err
}
}
},
}

View File

@@ -0,0 +1,125 @@
/*
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 images
import (
"io"
"os"
"github.com/containerd/containerd/cmd/ctr/commands"
oci "github.com/containerd/containerd/images/oci"
"github.com/containerd/containerd/reference"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/urfave/cli"
)
var exportCommand = cli.Command{
Name: "export",
Usage: "export an image",
ArgsUsage: "[flags] <out> <image>",
Description: `Export an image to a tar stream.
Currently, only OCI format is supported.
`,
Flags: []cli.Flag{
// TODO(AkihiroSuda): make this map[string]string as in moby/moby#33355?
cli.StringFlag{
Name: "oci-ref-name",
Value: "",
Usage: "override org.opencontainers.image.ref.name annotation",
},
cli.StringFlag{
Name: "manifest",
Usage: "digest of manifest",
},
cli.StringFlag{
Name: "manifest-type",
Usage: "media type of manifest digest",
Value: ocispec.MediaTypeImageManifest,
},
},
Action: func(context *cli.Context) error {
var (
out = context.Args().First()
local = context.Args().Get(1)
desc ocispec.Descriptor
)
if out == "" || local == "" {
return errors.New("please provide both an output filename and an image reference to export")
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
if manifest := context.String("manifest"); manifest != "" {
desc.Digest, err = digest.Parse(manifest)
if err != nil {
return errors.Wrap(err, "invalid manifest digest")
}
desc.MediaType = context.String("manifest-type")
} else {
img, err := client.ImageService().Get(ctx, local)
if err != nil {
return errors.Wrap(err, "unable to resolve image to manifest")
}
desc = img.Target
}
if desc.Annotations == nil {
desc.Annotations = make(map[string]string)
}
if s, ok := desc.Annotations[ocispec.AnnotationRefName]; !ok || s == "" {
if ociRefName := determineOCIRefName(local); ociRefName != "" {
desc.Annotations[ocispec.AnnotationRefName] = ociRefName
}
if ociRefName := context.String("oci-ref-name"); ociRefName != "" {
desc.Annotations[ocispec.AnnotationRefName] = ociRefName
}
}
var w io.WriteCloser
if out == "-" {
w = os.Stdout
} else {
w, err = os.Create(out)
if err != nil {
return nil
}
}
r, err := client.Export(ctx, &oci.V1Exporter{}, desc)
if err != nil {
return err
}
if _, err := io.Copy(w, r); err != nil {
return err
}
if err := w.Close(); err != nil {
return err
}
return r.Close()
},
}
func determineOCIRefName(local string) string {
refspec, err := reference.Parse(local)
if err != nil {
return ""
}
tag, _ := reference.SplitObject(refspec.Object)
return tag
}

View File

@@ -0,0 +1,330 @@
/*
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 images
import (
"fmt"
"os"
"sort"
"strings"
"text/tabwriter"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/platforms"
"github.com/containerd/containerd/progress"
"github.com/pkg/errors"
"github.com/urfave/cli"
)
// Command is the cli command for managing images
var Command = cli.Command{
Name: "images",
Aliases: []string{"image", "i"},
Usage: "manage images",
Subcommands: cli.Commands{
checkCommand,
exportCommand,
importCommand,
listCommand,
pullCommand,
pushCommand,
removeCommand,
setLabelsCommand,
},
}
var listCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "list images known to containerd",
ArgsUsage: "[flags] <ref>",
Description: "list images registered with containerd",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "quiet, q",
Usage: "print only the image refs",
},
},
Action: func(context *cli.Context) error {
var (
filters = context.Args()
quiet = context.Bool("quiet")
)
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
var (
imageStore = client.ImageService()
cs = client.ContentStore()
)
imageList, err := imageStore.List(ctx, filters...)
if err != nil {
return errors.Wrap(err, "failed to list images")
}
if quiet {
for _, image := range imageList {
fmt.Println(image.Name)
}
return nil
}
tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, ' ', 0)
fmt.Fprintln(tw, "REF\tTYPE\tDIGEST\tSIZE\tPLATFORMS\tLABELS\t")
for _, image := range imageList {
size, err := image.Size(ctx, cs, platforms.Default())
if err != nil {
log.G(ctx).WithError(err).Errorf("failed calculating size for image %s", image.Name)
}
platformColumn := "-"
specs, err := images.Platforms(ctx, cs, image.Target)
if err != nil {
log.G(ctx).WithError(err).Errorf("failed resolving platform for image %s", image.Name)
} else if len(specs) > 0 {
psm := map[string]struct{}{}
for _, p := range specs {
psm[platforms.Format(p)] = struct{}{}
}
var ps []string
for p := range psm {
ps = append(ps, p)
}
sort.Stable(sort.StringSlice(ps))
platformColumn = strings.Join(ps, ",")
}
labels := "-"
if len(image.Labels) > 0 {
var pairs []string
for k, v := range image.Labels {
pairs = append(pairs, fmt.Sprintf("%v=%v", k, v))
}
sort.Strings(pairs)
labels = strings.Join(pairs, ",")
}
fmt.Fprintf(tw, "%v\t%v\t%v\t%v\t%v\t%s\t\n",
image.Name,
image.Target.MediaType,
image.Target.Digest,
progress.Bytes(size),
platformColumn,
labels)
}
return tw.Flush()
},
}
var setLabelsCommand = cli.Command{
Name: "label",
Usage: "set and clear labels for an image",
ArgsUsage: "[flags] <name> [<key>=<value>, ...]",
Description: "set and clear labels for an image",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "replace-all, r",
Usage: "replace all labels",
},
},
Action: func(context *cli.Context) error {
var (
replaceAll = context.Bool("replace-all")
name, labels = commands.ObjectWithLabelArgs(context)
)
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
if name == "" {
return errors.New("please specify an image")
}
var (
is = client.ImageService()
fieldpaths []string
)
for k := range labels {
if replaceAll {
fieldpaths = append(fieldpaths, "labels")
} else {
fieldpaths = append(fieldpaths, strings.Join([]string{"labels", k}, "."))
}
}
image := images.Image{
Name: name,
Labels: labels,
}
updated, err := is.Update(ctx, image, fieldpaths...)
if err != nil {
return err
}
var labelStrings []string
for k, v := range updated.Labels {
labelStrings = append(labelStrings, fmt.Sprintf("%s=%s", k, v))
}
fmt.Println(strings.Join(labelStrings, ","))
return nil
},
}
var checkCommand = cli.Command{
Name: "check",
Usage: "check that an image has all content available locally",
ArgsUsage: "[flags] <ref> [<ref>, ...]",
Description: "check that an image has all content available locally",
Flags: commands.SnapshotterFlags,
Action: func(context *cli.Context) error {
var (
exitErr error
)
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
var (
contentStore = client.ContentStore()
tw = tabwriter.NewWriter(os.Stdout, 1, 8, 1, ' ', 0)
)
fmt.Fprintln(tw, "REF\tTYPE\tDIGEST\tSTATUS\tSIZE\tUNPACKED\t")
args := []string(context.Args())
imageList, err := client.ListImages(ctx, args...)
if err != nil {
return errors.Wrap(err, "failed listing images")
}
for _, image := range imageList {
var (
status string = "complete"
size string
requiredSize int64
presentSize int64
)
available, required, present, missing, err := images.Check(ctx, contentStore, image.Target(), platforms.Default())
if err != nil {
if exitErr == nil {
exitErr = errors.Wrapf(err, "unable to check %v", image.Name())
}
log.G(ctx).WithError(err).Errorf("unable to check %v", image.Name())
status = "error"
}
if status != "error" {
for _, d := range required {
requiredSize += d.Size
}
for _, d := range present {
presentSize += d.Size
}
if len(missing) > 0 {
status = "incomplete"
}
if available {
status += fmt.Sprintf(" (%v/%v)", len(present), len(required))
size = fmt.Sprintf("%v/%v", progress.Bytes(presentSize), progress.Bytes(requiredSize))
} else {
status = fmt.Sprintf("unavailable (%v/?)", len(present))
size = fmt.Sprintf("%v/?", progress.Bytes(presentSize))
}
} else {
size = "-"
}
unpacked, err := image.IsUnpacked(ctx, context.String("snapshotter"))
if err != nil {
if exitErr == nil {
exitErr = errors.Wrapf(err, "unable to check unpack for %v", image.Name())
}
log.G(ctx).WithError(err).Errorf("unable to check unpack for %v", image.Name())
}
fmt.Fprintf(tw, "%v\t%v\t%v\t%v\t%v\t%t\n",
image.Name(),
image.Target().MediaType,
image.Target().Digest,
status,
size,
unpacked)
}
tw.Flush()
return exitErr
},
}
var removeCommand = cli.Command{
Name: "remove",
Aliases: []string{"rm"},
Usage: "remove one or more images by reference",
ArgsUsage: "[flags] <ref> [<ref>, ...]",
Description: "remove one or more images by reference",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "sync",
Usage: "Synchronously remove image and all associated resources",
},
},
Action: func(context *cli.Context) error {
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
var (
exitErr error
imageStore = client.ImageService()
)
for i, target := range context.Args() {
var opts []images.DeleteOpt
if context.Bool("sync") && i == context.NArg()-1 {
opts = append(opts, images.SynchronousDelete())
}
if err := imageStore.Delete(ctx, target, opts...); err != nil {
if !errdefs.IsNotFound(err) {
if exitErr == nil {
exitErr = errors.Wrapf(err, "unable to delete %v", target)
}
log.G(ctx).WithError(err).Errorf("unable to delete %v", target)
continue
}
// image ref not found in metadata store; log not found condition
log.G(ctx).Warnf("%v: image not found", target)
} else {
fmt.Println(target)
}
}
return exitErr
},
}

View File

@@ -0,0 +1,114 @@
/*
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 images
import (
"fmt"
"io"
"os"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/images"
oci "github.com/containerd/containerd/images/oci"
"github.com/containerd/containerd/log"
"github.com/urfave/cli"
)
var importCommand = cli.Command{
Name: "import",
Usage: "import images",
ArgsUsage: "[flags] <in>",
Description: `Import images from a tar stream.
Implemented formats:
- oci.v1 (default)
For oci.v1 format, you need to specify --oci-name because an OCI archive contains image refs (tags)
but does not contain the base image name.
e.g.
$ ctr images import --format oci.v1 --oci-name foo/bar foobar.tar
If foobar.tar contains an OCI ref named "latest" and anonymous ref "sha256:deadbeef", the command will create
"foo/bar:latest" and "foo/bar@sha256:deadbeef" images in the containerd store.
`,
Flags: append([]cli.Flag{
cli.StringFlag{
Name: "format",
Value: "oci.v1",
Usage: "image format. See DESCRIPTION.",
},
cli.StringFlag{
Name: "oci-name",
Value: "unknown/unknown",
Usage: "prefix added to either oci.v1 ref annotation or digest",
},
// TODO(AkihiroSuda): support commands.LabelFlag (for all children objects)
}, commands.SnapshotterFlags...),
Action: func(context *cli.Context) error {
var (
in = context.Args().First()
imageImporter images.Importer
)
switch format := context.String("format"); format {
case "oci.v1":
imageImporter = &oci.V1Importer{
ImageName: context.String("oci-name"),
}
default:
return fmt.Errorf("unknown format %s", format)
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
var r io.ReadCloser
if in == "-" {
r = os.Stdin
} else {
r, err = os.Open(in)
if err != nil {
return err
}
}
imgs, err := client.Import(ctx, imageImporter, r)
if err != nil {
return err
}
if err = r.Close(); err != nil {
return err
}
log.G(ctx).Debugf("unpacking %d images", len(imgs))
for _, img := range imgs {
// TODO: Show unpack status
fmt.Printf("unpacking %s (%s)...", img.Name(), img.Target().Digest)
err = img.Unpack(ctx, context.String("snapshotter"))
if err != nil {
return err
}
fmt.Println("done")
}
return nil
},
}

View File

@@ -0,0 +1,78 @@
/*
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 images
import (
"fmt"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/cmd/ctr/commands/content"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/platforms"
"github.com/urfave/cli"
)
var pullCommand = cli.Command{
Name: "pull",
Usage: "pull an image from a remote",
ArgsUsage: "[flags] <ref>",
Description: `Fetch and prepare an image for use in containerd.
After pulling an image, it should be ready to use the same reference in a run
command. As part of this process, we do the following:
1. Fetch all resources into containerd.
2. Prepare the snapshot filesystem with the pulled resources.
3. Register metadata for the image.
`,
Flags: append(append(commands.RegistryFlags, append(commands.SnapshotterFlags, commands.LabelFlag)...),
cli.StringSliceFlag{
Name: "platform",
Usage: "Pull content from a specific platform",
Value: &cli.StringSlice{platforms.Default()},
},
cli.BoolFlag{
Name: "all-platforms",
Usage: "pull content from all platforms",
},
),
Action: func(context *cli.Context) error {
var (
ref = context.Args().First()
)
if ref == "" {
return fmt.Errorf("please provide an image reference to pull")
}
ctx, cancel := commands.AppContext(context)
defer cancel()
img, err := content.Fetch(ref, context)
if err != nil {
return err
}
log.G(ctx).WithField("image", ref).Debug("unpacking")
// TODO: Show unpack status
fmt.Printf("unpacking %s...\n", img.Target().Digest)
err = img.Unpack(ctx, context.String("snapshotter"))
if err == nil {
fmt.Println("done")
}
return err
},
}

View File

@@ -0,0 +1,212 @@
/*
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 images
import (
gocontext "context"
"os"
"sync"
"text/tabwriter"
"time"
"github.com/containerd/containerd"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/cmd/ctr/commands/content"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/progress"
"github.com/containerd/containerd/remotes"
"github.com/containerd/containerd/remotes/docker"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/urfave/cli"
"golang.org/x/sync/errgroup"
)
var pushCommand = cli.Command{
Name: "push",
Usage: "push an image to a remote",
ArgsUsage: "[flags] <remote> [<local>]",
Description: `Pushes an image reference from containerd.
All resources associated with the manifest reference will be pushed.
The ref is used to resolve to a locally existing image manifest.
The image manifest must exist before push. Creating a new image
manifest can be done through calculating the diff for layers,
creating the associated configuration, and creating the manifest
which references those resources.
`,
Flags: append(commands.RegistryFlags, cli.StringFlag{
Name: "manifest",
Usage: "digest of manifest",
}, cli.StringFlag{
Name: "manifest-type",
Usage: "media type of manifest digest",
Value: ocispec.MediaTypeImageManifest,
}),
Action: func(context *cli.Context) error {
var (
ref = context.Args().First()
local = context.Args().Get(1)
desc ocispec.Descriptor
)
if ref == "" {
return errors.New("please provide a remote image reference to push")
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
if manifest := context.String("manifest"); manifest != "" {
desc.Digest, err = digest.Parse(manifest)
if err != nil {
return errors.Wrap(err, "invalid manifest digest")
}
desc.MediaType = context.String("manifest-type")
} else {
if local == "" {
local = ref
}
img, err := client.ImageService().Get(ctx, local)
if err != nil {
return errors.Wrap(err, "unable to resolve image to manifest")
}
desc = img.Target
}
resolver, err := commands.GetResolver(ctx, context)
if err != nil {
return err
}
ongoing := newPushJobs(commands.PushTracker)
eg, ctx := errgroup.WithContext(ctx)
eg.Go(func() error {
log.G(ctx).WithField("image", ref).WithField("digest", desc.Digest).Debug("pushing")
jobHandler := images.HandlerFunc(func(ctx gocontext.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
ongoing.add(remotes.MakeRefKey(ctx, desc))
return nil, nil
})
return client.Push(ctx, ref, desc,
containerd.WithResolver(resolver),
containerd.WithImageHandler(jobHandler),
)
})
errs := make(chan error)
go func() {
defer close(errs)
errs <- eg.Wait()
}()
var (
ticker = time.NewTicker(100 * time.Millisecond)
fw = progress.NewWriter(os.Stdout)
start = time.Now()
done bool
)
defer ticker.Stop()
for {
select {
case <-ticker.C:
fw.Flush()
tw := tabwriter.NewWriter(fw, 1, 8, 1, ' ', 0)
content.Display(tw, ongoing.status(), start)
tw.Flush()
if done {
fw.Flush()
return nil
}
case err := <-errs:
if err != nil {
return err
}
done = true
case <-ctx.Done():
done = true // allow ui to update once more
}
}
},
}
type pushjobs struct {
jobs map[string]struct{}
ordered []string
tracker docker.StatusTracker
mu sync.Mutex
}
func newPushJobs(tracker docker.StatusTracker) *pushjobs {
return &pushjobs{
jobs: make(map[string]struct{}),
tracker: tracker,
}
}
func (j *pushjobs) add(ref string) {
j.mu.Lock()
defer j.mu.Unlock()
if _, ok := j.jobs[ref]; ok {
return
}
j.ordered = append(j.ordered, ref)
j.jobs[ref] = struct{}{}
}
func (j *pushjobs) status() []content.StatusInfo {
j.mu.Lock()
defer j.mu.Unlock()
statuses := make([]content.StatusInfo, 0, len(j.jobs))
for _, name := range j.ordered {
si := content.StatusInfo{
Ref: name,
}
status, err := j.tracker.GetStatus(name)
if err != nil {
si.Status = "waiting"
} else {
si.Offset = status.Offset
si.Total = status.Total
si.StartedAt = status.StartedAt
si.UpdatedAt = status.UpdatedAt
if status.Offset >= status.Total {
if status.UploadUUID == "" {
si.Status = "done"
} else {
si.Status = "committing"
}
} else {
si.Status = "uploading"
}
}
statuses = append(statuses, si)
}
return statuses
}

View File

@@ -0,0 +1,173 @@
/*
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 namespaces
import (
"fmt"
"os"
"sort"
"strings"
"text/tabwriter"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/log"
"github.com/pkg/errors"
"github.com/urfave/cli"
)
// Command is the cli command for managing namespaces
var Command = cli.Command{
Name: "namespaces",
Aliases: []string{"namespace"},
Usage: "manage namespaces",
Subcommands: cli.Commands{
createCommand,
listCommand,
removeCommand,
setLabelsCommand,
},
}
var createCommand = cli.Command{
Name: "create",
Usage: "create a new namespace",
ArgsUsage: "<name> [<key>=<value]",
Description: "create a new namespace. it must be unique",
Action: func(context *cli.Context) error {
namespace, labels := commands.ObjectWithLabelArgs(context)
if namespace == "" {
return errors.New("please specify a namespace")
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
namespaces := client.NamespaceService()
return namespaces.Create(ctx, namespace, labels)
},
}
var setLabelsCommand = cli.Command{
Name: "label",
Usage: "set and clear labels for a namespace",
ArgsUsage: "<name> [<key>=<value>, ...]",
Description: "set and clear labels for a namespace",
Action: func(context *cli.Context) error {
namespace, labels := commands.ObjectWithLabelArgs(context)
if namespace == "" {
return errors.New("please specify a namespace")
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
namespaces := client.NamespaceService()
for k, v := range labels {
if err := namespaces.SetLabel(ctx, namespace, k, v); err != nil {
return err
}
}
return nil
},
}
var listCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "list namespaces",
ArgsUsage: "[flags]",
Description: "list namespaces",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "quiet, q",
Usage: "print only the namespace name",
},
},
Action: func(context *cli.Context) error {
quiet := context.Bool("quiet")
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
namespaces := client.NamespaceService()
nss, err := namespaces.List(ctx)
if err != nil {
return err
}
if quiet {
for _, ns := range nss {
fmt.Println(ns)
}
return nil
}
tw := tabwriter.NewWriter(os.Stdout, 1, 8, 1, ' ', 0)
fmt.Fprintln(tw, "NAME\tLABELS\t")
for _, ns := range nss {
labels, err := namespaces.Labels(ctx, ns)
if err != nil {
return err
}
var labelStrings []string
for k, v := range labels {
labelStrings = append(labelStrings, strings.Join([]string{k, v}, "="))
}
sort.Strings(labelStrings)
fmt.Fprintf(tw, "%v\t%v\t\n", ns, strings.Join(labelStrings, ","))
}
return tw.Flush()
},
}
var removeCommand = cli.Command{
Name: "remove",
Aliases: []string{"rm"},
Usage: "remove one or more namespaces",
ArgsUsage: "<name> [<name>, ...]",
Description: "remove one or more namespaces. for now, the namespace must be empty",
Action: func(context *cli.Context) error {
var exitErr error
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
namespaces := client.NamespaceService()
for _, target := range context.Args() {
if err := namespaces.Delete(ctx, target); err != nil {
if !errdefs.IsNotFound(err) {
if exitErr == nil {
exitErr = errors.Wrapf(err, "unable to delete %v", target)
}
log.G(ctx).WithError(err).Errorf("unable to delete %v", target)
continue
}
}
fmt.Println(target)
}
return exitErr
},
}

View File

@@ -0,0 +1,153 @@
/*
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 plugins
import (
"fmt"
"os"
"sort"
"strings"
"text/tabwriter"
introspection "github.com/containerd/containerd/api/services/introspection/v1"
"github.com/containerd/containerd/api/types"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/platforms"
"github.com/opencontainers/image-spec/specs-go/v1"
"github.com/urfave/cli"
"google.golang.org/grpc/codes"
)
// Command is a cli command that outputs plugin information
var Command = cli.Command{
Name: "plugins",
Aliases: []string{"plugin"},
Usage: "provides information about containerd plugins",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "quiet,q",
Usage: "print only the plugin ids",
},
cli.BoolFlag{
Name: "detailed,d",
Usage: "print detailed information about each plugin",
},
},
Action: func(context *cli.Context) error {
var (
quiet = context.Bool("quiet")
detailed = context.Bool("detailed")
)
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
ps := client.IntrospectionService()
response, err := ps.Plugins(ctx, &introspection.PluginsRequest{
Filters: context.Args(),
})
if err != nil {
return err
}
if quiet {
for _, plugin := range response.Plugins {
fmt.Println(plugin.ID)
}
return nil
}
w := tabwriter.NewWriter(os.Stdout, 4, 8, 4, ' ', 0)
if detailed {
first := true
for _, plugin := range response.Plugins {
if !first {
fmt.Fprintln(w, "\t\t\t")
}
first = false
fmt.Fprintln(w, "Type:\t", plugin.Type)
fmt.Fprintln(w, "ID:\t", plugin.ID)
if len(plugin.Requires) > 0 {
fmt.Fprintln(w, "Requires:\t")
for _, r := range plugin.Requires {
fmt.Fprintln(w, "\t", r)
}
}
if len(plugin.Platforms) > 0 {
fmt.Fprintln(w, "Platforms:\t", prettyPlatforms(plugin.Platforms))
}
if len(plugin.Exports) > 0 {
fmt.Fprintln(w, "Exports:\t")
for k, v := range plugin.Exports {
fmt.Fprintln(w, "\t", k, "\t", v)
}
}
if len(plugin.Capabilities) > 0 {
fmt.Fprintln(w, "Capabilities:\t", strings.Join(plugin.Capabilities, ","))
}
if plugin.InitErr != nil {
fmt.Fprintln(w, "Error:\t")
fmt.Fprintln(w, "\t Code:\t", codes.Code(plugin.InitErr.Code))
fmt.Fprintln(w, "\t Message:\t", plugin.InitErr.Message)
}
}
return w.Flush()
}
fmt.Fprintln(w, "TYPE\tID\tPLATFORMS\tSTATUS\t")
for _, plugin := range response.Plugins {
status := "ok"
if plugin.InitErr != nil {
status = "error"
}
var platformColumn = "-"
if len(plugin.Platforms) > 0 {
platformColumn = prettyPlatforms(plugin.Platforms)
}
if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t\n",
plugin.Type,
plugin.ID,
platformColumn,
status,
); err != nil {
return err
}
}
return w.Flush()
},
}
func prettyPlatforms(pspb []types.Platform) string {
psm := map[string]struct{}{}
for _, p := range pspb {
psm[platforms.Format(v1.Platform{
OS: p.OS,
Architecture: p.Architecture,
Variant: p.Variant,
})] = struct{}{}
}
var ps []string
for p := range psm {
ps = append(ps, p)
}
sort.Stable(sort.StringSlice(ps))
return strings.Join(ps, ",")
}

View File

@@ -0,0 +1,181 @@
/*
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 pprof
import (
"fmt"
"io"
"net/http"
"os"
"time"
"github.com/containerd/containerd/defaults"
"github.com/pkg/errors"
"github.com/urfave/cli"
)
type pprofDialer struct {
proto string
addr string
}
// Command is the cli command for providing golang pprof outputs for containerd
var Command = cli.Command{
Name: "pprof",
Usage: "provide golang pprof outputs for containerd",
Flags: []cli.Flag{
cli.StringFlag{
Name: "debug-socket, d",
Usage: "socket path for containerd's debug server",
Value: defaults.DefaultDebugAddress,
},
},
Subcommands: []cli.Command{
pprofBlockCommand,
pprofGoroutinesCommand,
pprofHeapCommand,
pprofProfileCommand,
pprofThreadcreateCommand,
pprofTraceCommand,
},
}
var pprofGoroutinesCommand = cli.Command{
Name: "goroutines",
Usage: "dump goroutine stack dump",
Action: func(context *cli.Context) error {
client := getPProfClient(context)
output, err := httpGetRequest(client, "/debug/pprof/goroutine?debug=2")
if err != nil {
return err
}
defer output.Close()
_, err = io.Copy(os.Stdout, output)
return err
},
}
var pprofHeapCommand = cli.Command{
Name: "heap",
Usage: "dump heap profile",
Action: func(context *cli.Context) error {
client := getPProfClient(context)
output, err := httpGetRequest(client, "/debug/pprof/heap")
if err != nil {
return err
}
defer output.Close()
_, err = io.Copy(os.Stdout, output)
return err
},
}
var pprofProfileCommand = cli.Command{
Name: "profile",
Usage: "CPU profile",
Action: func(context *cli.Context) error {
client := getPProfClient(context)
output, err := httpGetRequest(client, "/debug/pprof/profile")
if err != nil {
return err
}
defer output.Close()
_, err = io.Copy(os.Stdout, output)
return err
},
}
var pprofTraceCommand = cli.Command{
Name: "trace",
Usage: "collect execution trace",
Flags: []cli.Flag{
cli.DurationFlag{
Name: "seconds,s",
Usage: "trace time (seconds)",
Value: 5 * time.Second,
},
},
Action: func(context *cli.Context) error {
client := getPProfClient(context)
seconds := context.Duration("seconds").Seconds()
uri := fmt.Sprintf("/debug/pprof/trace?seconds=%v", seconds)
output, err := httpGetRequest(client, uri)
if err != nil {
return err
}
defer output.Close()
_, err = io.Copy(os.Stdout, output)
return err
},
}
var pprofBlockCommand = cli.Command{
Name: "block",
Usage: "goroutine blocking profile",
Action: func(context *cli.Context) error {
client := getPProfClient(context)
output, err := httpGetRequest(client, "/debug/pprof/block")
if err != nil {
return err
}
defer output.Close()
_, err = io.Copy(os.Stdout, output)
return err
},
}
var pprofThreadcreateCommand = cli.Command{
Name: "threadcreate",
Usage: "goroutine thread creating profile",
Action: func(context *cli.Context) error {
client := getPProfClient(context)
output, err := httpGetRequest(client, "/debug/pprof/threadcreate")
if err != nil {
return err
}
defer output.Close()
_, err = io.Copy(os.Stdout, output)
return err
},
}
func getPProfClient(context *cli.Context) *http.Client {
dialer := getPProfDialer(context.GlobalString("debug-socket"))
tr := &http.Transport{
Dial: dialer.pprofDial,
}
client := &http.Client{Transport: tr}
return client
}
func httpGetRequest(client *http.Client, request string) (io.ReadCloser, error) {
resp, err := client.Get("http://." + request)
if err != nil {
return nil, err
}
if resp.StatusCode != 200 {
return nil, errors.Errorf("http get failed with status: %s", resp.Status)
}
return resp.Body, nil
}

View File

@@ -0,0 +1,29 @@
// +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 pprof
import "net"
func (d *pprofDialer) pprofDial(proto, addr string) (conn net.Conn, err error) {
return net.Dial(d.proto, d.addr)
}
func getPProfDialer(addr string) *pprofDialer {
return &pprofDialer{"unix", addr}
}

View 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 pprof
import (
"net"
winio "github.com/Microsoft/go-winio"
)
func (d *pprofDialer) pprofDial(proto, addr string) (conn net.Conn, err error) {
return winio.DialPipe(d.addr, nil)
}
func getPProfDialer(addr string) *pprofDialer {
return &pprofDialer{"winpipe", addr}
}

View File

@@ -0,0 +1,108 @@
/*
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 commands
import (
"bufio"
gocontext "context"
"crypto/tls"
"fmt"
"net"
"net/http"
"strings"
"time"
"github.com/containerd/console"
"github.com/containerd/containerd/remotes"
"github.com/containerd/containerd/remotes/docker"
"github.com/pkg/errors"
"github.com/urfave/cli"
)
// PushTracker returns a new InMemoryTracker which tracks the ref status
var PushTracker = docker.NewInMemoryTracker()
func passwordPrompt() (string, error) {
c := console.Current()
defer c.Reset()
if err := c.DisableEcho(); err != nil {
return "", errors.Wrap(err, "failed to disable echo")
}
line, _, err := bufio.NewReader(c).ReadLine()
if err != nil {
return "", errors.Wrap(err, "failed to read line")
}
return string(line), nil
}
// GetResolver prepares the resolver from the environment and options
func GetResolver(ctx gocontext.Context, clicontext *cli.Context) (remotes.Resolver, error) {
username := clicontext.String("user")
var secret string
if i := strings.IndexByte(username, ':'); i > 0 {
secret = username[i+1:]
username = username[0:i]
}
options := docker.ResolverOptions{
PlainHTTP: clicontext.Bool("plain-http"),
Tracker: PushTracker,
}
if username != "" {
if secret == "" {
fmt.Printf("Password: ")
var err error
secret, err = passwordPrompt()
if err != nil {
return nil, err
}
fmt.Print("\n")
}
} else if rt := clicontext.String("refresh"); rt != "" {
secret = rt
}
options.Credentials = func(host string) (string, string, error) {
// Only one host
return username, secret, nil
}
tr := &http.Transport{
Proxy: http.ProxyFromEnvironment,
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 10,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: clicontext.Bool("insecure"),
},
ExpectContinueTimeout: 5 * time.Second,
}
options.Client = &http.Client{
Transport: tr,
}
return docker.NewResolver(options), nil
}

View File

@@ -0,0 +1,258 @@
/*
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 run
import (
gocontext "context"
"encoding/csv"
"encoding/json"
"fmt"
"io/ioutil"
"runtime"
"strings"
"github.com/containerd/console"
"github.com/containerd/containerd"
"github.com/containerd/containerd/cio"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/cmd/ctr/commands/tasks"
"github.com/containerd/containerd/containers"
"github.com/containerd/containerd/oci"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
// ContainerFlags are cli flags specifying container options
var ContainerFlags = []cli.Flag{
cli.StringFlag{
Name: "config,c",
Usage: "path to the runtime-specific spec config file",
},
cli.StringFlag{
Name: "checkpoint",
Usage: "provide the checkpoint digest to restore the container",
},
cli.StringFlag{
Name: "cwd",
Usage: "specify the working directory of the process",
},
cli.StringSliceFlag{
Name: "env",
Usage: "specify additional container environment variables (i.e. FOO=bar)",
},
cli.StringSliceFlag{
Name: "label",
Usage: "specify additional labels (i.e. foo=bar)",
},
cli.StringSliceFlag{
Name: "mount",
Usage: "specify additional container mount (ex: type=bind,src=/tmp,dest=/host,options=rbind:ro)",
},
cli.BoolFlag{
Name: "net-host",
Usage: "enable host networking for the container",
},
cli.BoolFlag{
Name: "read-only",
Usage: "set the containers filesystem as readonly",
},
cli.StringFlag{
Name: "runtime",
Usage: "runtime name (io.containerd.runtime.v1.linux, io.containerd.runtime.v1.windows, io.containerd.runtime.v1.com.vmware.linux)",
Value: fmt.Sprintf("io.containerd.runtime.v1.%s", runtime.GOOS),
},
cli.BoolFlag{
Name: "tty,t",
Usage: "allocate a TTY for the container",
},
cli.StringSliceFlag{
Name: "with-ns",
Usage: "specify existing Linux namespaces to join at container runtime (format '<nstype>:<path>')",
},
cli.StringFlag{
Name: "pid-file",
Usage: "file path to write the task's pid",
},
}
func loadSpec(path string, s *specs.Spec) error {
raw, err := ioutil.ReadFile(path)
if err != nil {
return errors.New("cannot load spec config file")
}
if err := json.Unmarshal(raw, s); err != nil {
return errors.Errorf("decoding spec config file failed, current supported OCI runtime-spec : v%s", specs.Version)
}
return nil
}
func withMounts(context *cli.Context) oci.SpecOpts {
return func(ctx gocontext.Context, client oci.Client, container *containers.Container, s *specs.Spec) error {
mounts := make([]specs.Mount, 0)
for _, mount := range context.StringSlice("mount") {
m, err := parseMountFlag(mount)
if err != nil {
return err
}
mounts = append(mounts, m)
}
return oci.WithMounts(mounts)(ctx, client, container, s)
}
}
// parseMountFlag parses a mount string in the form "type=foo,source=/path,destination=/target,options=rbind:rw"
func parseMountFlag(m string) (specs.Mount, error) {
mount := specs.Mount{}
r := csv.NewReader(strings.NewReader(m))
fields, err := r.Read()
if err != nil {
return mount, err
}
for _, field := range fields {
v := strings.Split(field, "=")
if len(v) != 2 {
return mount, fmt.Errorf("invalid mount specification: expected key=val")
}
key := v[0]
val := v[1]
switch key {
case "type":
mount.Type = val
case "source", "src":
mount.Source = val
case "destination", "dst":
mount.Destination = val
case "options":
mount.Options = strings.Split(val, ":")
default:
return mount, fmt.Errorf("mount option %q not supported", key)
}
}
return mount, nil
}
// Command runs a container
var Command = cli.Command{
Name: "run",
Usage: "run a container",
ArgsUsage: "[flags] Image|RootFS ID [COMMAND] [ARG...]",
Flags: append([]cli.Flag{
cli.BoolFlag{
Name: "rm",
Usage: "remove the container after running",
},
cli.BoolFlag{
Name: "null-io",
Usage: "send all IO to /dev/null",
},
cli.BoolFlag{
Name: "detach,d",
Usage: "detach from the task after it has started execution",
},
cli.StringFlag{
Name: "fifo-dir",
Usage: "directory used for storing IO FIFOs",
},
}, append(commands.SnapshotterFlags, ContainerFlags...)...),
Action: func(context *cli.Context) error {
var (
err error
id = context.Args().Get(1)
ref = context.Args().First()
tty = context.Bool("tty")
detach = context.Bool("detach")
)
if ref == "" {
return errors.New("image ref must be provided")
}
if id == "" {
return errors.New("container id must be provided")
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
container, err := NewContainer(ctx, client, context)
if err != nil {
return err
}
if context.Bool("rm") && !detach {
defer container.Delete(ctx, containerd.WithSnapshotCleanup)
}
opts := getNewTaskOpts(context)
ioOpts := []cio.Opt{cio.WithFIFODir(context.String("fifo-dir"))}
task, err := tasks.NewTask(ctx, client, container, context.String("checkpoint"), tty, context.Bool("null-io"), ioOpts, opts...)
if err != nil {
return err
}
var statusC <-chan containerd.ExitStatus
if !detach {
defer task.Delete(ctx)
if statusC, err = task.Wait(ctx); err != nil {
return err
}
}
if context.IsSet("pid-file") {
if err := commands.WritePidFile(context.String("pid-file"), int(task.Pid())); err != nil {
return err
}
}
var con console.Console
if tty {
con = console.Current()
defer con.Reset()
if err := con.SetRaw(); err != nil {
return err
}
}
if err := task.Start(ctx); err != nil {
return err
}
if detach {
return nil
}
if tty {
if err := tasks.HandleConsoleResize(ctx, task, con); err != nil {
logrus.WithError(err).Error("console resize")
}
} else {
sigc := commands.ForwardAllSignals(ctx, task)
defer commands.StopCatch(sigc)
}
status := <-statusC
code, _, err := status.Result()
if err != nil {
return err
}
if _, err := task.Delete(ctx); err != nil {
return err
}
if code != 0 {
return cli.NewExitError("", int(code))
}
return nil
},
}

View File

@@ -0,0 +1,161 @@
// +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 run
import (
gocontext "context"
"strings"
"github.com/containerd/containerd"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/oci"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/urfave/cli"
)
func init() {
ContainerFlags = append(ContainerFlags, cli.BoolFlag{
Name: "rootfs",
Usage: "use custom rootfs that is not managed by containerd snapshotter",
}, cli.BoolFlag{
Name: "no-pivot",
Usage: "disable use of pivot-root (linux only)",
})
}
// NewContainer creates a new container
func NewContainer(ctx gocontext.Context, client *containerd.Client, context *cli.Context) (containerd.Container, error) {
var (
ref = context.Args().First()
id = context.Args().Get(1)
args = context.Args()[2:]
)
if raw := context.String("checkpoint"); raw != "" {
im, err := client.GetImage(ctx, raw)
if err != nil {
return nil, err
}
return client.NewContainer(ctx, id, containerd.WithCheckpoint(im, id))
}
var (
opts []oci.SpecOpts
cOpts []containerd.NewContainerOpts
spec containerd.NewContainerOpts
)
opts = append(opts, oci.WithEnv(context.StringSlice("env")))
opts = append(opts, withMounts(context))
cOpts = append(cOpts, containerd.WithContainerLabels(commands.LabelArgs(context.StringSlice("label"))))
cOpts = append(cOpts, containerd.WithRuntime(context.String("runtime"), nil))
if context.Bool("rootfs") {
opts = append(opts, oci.WithRootFSPath(ref))
} else {
snapshotter := context.String("snapshotter")
image, err := client.GetImage(ctx, ref)
if err != nil {
return nil, err
}
unpacked, err := image.IsUnpacked(ctx, snapshotter)
if err != nil {
return nil, err
}
if !unpacked {
if err := image.Unpack(ctx, snapshotter); err != nil {
return nil, err
}
}
opts = append(opts, oci.WithImageConfig(image))
cOpts = append(cOpts,
containerd.WithImage(image),
containerd.WithSnapshotter(snapshotter),
// Even when "readonly" is set, we don't use KindView snapshot here. (#1495)
// We pass writable snapshot to the OCI runtime, and the runtime remounts it as read-only,
// after creating some mount points on demand.
containerd.WithNewSnapshot(id, image))
}
if context.Bool("readonly") {
opts = append(opts, oci.WithRootFSReadonly())
}
if len(args) > 0 {
opts = append(opts, oci.WithProcessArgs(args...))
}
if cwd := context.String("cwd"); cwd != "" {
opts = append(opts, oci.WithProcessCwd(cwd))
}
if context.Bool("tty") {
opts = append(opts, oci.WithTTY)
}
if context.Bool("net-host") {
opts = append(opts, oci.WithHostNamespace(specs.NetworkNamespace), oci.WithHostHostsFile, oci.WithHostResolvconf)
}
joinNs := context.StringSlice("with-ns")
for _, ns := range joinNs {
parts := strings.Split(ns, ":")
if len(parts) != 2 {
return nil, errors.New("joining a Linux namespace using --with-ns requires the format 'nstype:path'")
}
if !validNamespace(parts[0]) {
return nil, errors.New("the Linux namespace type specified in --with-ns is not valid: " + parts[0])
}
opts = append(opts, oci.WithLinuxNamespace(specs.LinuxNamespace{
Type: specs.LinuxNamespaceType(parts[0]),
Path: parts[1],
}))
}
if context.IsSet("config") {
var s specs.Spec
if err := loadSpec(context.String("config"), &s); err != nil {
return nil, err
}
spec = containerd.WithSpec(&s, opts...)
} else {
spec = containerd.WithNewSpec(opts...)
}
cOpts = append(cOpts, spec)
// oci.WithImageConfig (WithUsername, WithUserID) depends on rootfs snapshot for resolving /etc/passwd.
// So cOpts needs to have precedence over opts.
// TODO: WithUsername, WithUserID should additionally support non-snapshot rootfs
return client.NewContainer(ctx, id, cOpts...)
}
func getNewTaskOpts(context *cli.Context) []containerd.NewTaskOpts {
if context.Bool("no-pivot") {
return []containerd.NewTaskOpts{containerd.WithNoPivotRoot}
}
return nil
}
func validNamespace(ns string) bool {
linuxNs := specs.LinuxNamespaceType(ns)
switch linuxNs {
case specs.PIDNamespace,
specs.NetworkNamespace,
specs.UTSNamespace,
specs.MountNamespace,
specs.UserNamespace,
specs.IPCNamespace,
specs.CgroupNamespace:
return true
default:
return false
}
}

View File

@@ -0,0 +1,99 @@
/*
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 run
import (
gocontext "context"
"github.com/containerd/console"
"github.com/containerd/containerd"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/containers"
"github.com/containerd/containerd/oci"
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
func withTTY(terminal bool) oci.SpecOpts {
if !terminal {
return func(ctx gocontext.Context, client oci.Client, c *containers.Container, s *specs.Spec) error {
s.Process.Terminal = false
return nil
}
}
con := console.Current()
size, err := con.Size()
if err != nil {
logrus.WithError(err).Error("console size")
}
return oci.WithTTY(int(size.Width), int(size.Height))
}
// NewContainer creates a new container
func NewContainer(ctx gocontext.Context, client *containerd.Client, context *cli.Context) (containerd.Container, error) {
var (
ref = context.Args().First()
id = context.Args().Get(1)
args = context.Args()[2:]
)
image, err := client.GetImage(ctx, ref)
if err != nil {
return nil, err
}
var (
opts []oci.SpecOpts
cOpts []containerd.NewContainerOpts
spec containerd.NewContainerOpts
)
opts = append(opts, oci.WithImageConfig(image))
opts = append(opts, oci.WithEnv(context.StringSlice("env")))
opts = append(opts, withMounts(context))
opts = append(opts, withTTY(context.Bool("tty")))
if len(args) > 0 {
opts = append(opts, oci.WithProcessArgs(args...))
}
if cwd := context.String("cwd"); cwd != "" {
opts = append(opts, oci.WithProcessCwd(cwd))
}
if context.IsSet("config") {
var s specs.Spec
if err := loadSpec(context.String("config"), &s); err != nil {
return nil, err
}
spec = containerd.WithSpec(&s, opts...)
} else {
spec = containerd.WithNewSpec(opts...)
}
cOpts = append(cOpts, containerd.WithContainerLabels(commands.LabelArgs(context.StringSlice("label"))))
cOpts = append(cOpts, containerd.WithImage(image))
cOpts = append(cOpts, containerd.WithSnapshotter(context.String("snapshotter")))
cOpts = append(cOpts, containerd.WithNewSnapshot(id, image))
cOpts = append(cOpts, containerd.WithRuntime(context.String("runtime"), nil))
cOpts = append(cOpts, spec)
return client.NewContainer(ctx, id, cOpts...)
}
func getNewTaskOpts(_ *cli.Context) []containerd.NewTaskOpts {
return nil
}

View File

@@ -0,0 +1,93 @@
// +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 shim
import (
gocontext "context"
"io"
"os"
"sync"
"github.com/containerd/fifo"
"golang.org/x/sys/unix"
)
var bufPool = sync.Pool{
New: func() interface{} {
buffer := make([]byte, 32<<10)
return &buffer
},
}
func prepareStdio(stdin, stdout, stderr string, console bool) (wg *sync.WaitGroup, err error) {
wg = &sync.WaitGroup{}
ctx := gocontext.Background()
f, err := fifo.OpenFifo(ctx, stdin, unix.O_WRONLY|unix.O_CREAT|unix.O_NONBLOCK, 0700)
if err != nil {
return nil, err
}
defer func(c io.Closer) {
if err != nil {
c.Close()
}
}(f)
go func(w io.WriteCloser) {
p := bufPool.Get().(*[]byte)
defer bufPool.Put(p)
io.CopyBuffer(w, os.Stdin, *p)
w.Close()
}(f)
f, err = fifo.OpenFifo(ctx, stdout, unix.O_RDONLY|unix.O_CREAT|unix.O_NONBLOCK, 0700)
if err != nil {
return nil, err
}
defer func(c io.Closer) {
if err != nil {
c.Close()
}
}(f)
wg.Add(1)
go func(r io.ReadCloser) {
io.Copy(os.Stdout, r)
r.Close()
wg.Done()
}(f)
f, err = fifo.OpenFifo(ctx, stderr, unix.O_RDONLY|unix.O_CREAT|unix.O_NONBLOCK, 0700)
if err != nil {
return nil, err
}
defer func(c io.Closer) {
if err != nil {
c.Close()
}
}(f)
if !console {
wg.Add(1)
go func(r io.ReadCloser) {
io.Copy(os.Stderr, r)
r.Close()
wg.Done()
}(f)
}
return wg, nil
}

View File

@@ -0,0 +1,246 @@
// +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 shim
import (
"fmt"
"io/ioutil"
"net"
gocontext "context"
"github.com/containerd/console"
"github.com/containerd/containerd/cmd/ctr/commands"
shim "github.com/containerd/containerd/linux/shim/v1"
"github.com/containerd/typeurl"
ptypes "github.com/gogo/protobuf/types"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/stevvooe/ttrpc"
"github.com/urfave/cli"
)
var empty = &ptypes.Empty{}
var fifoFlags = []cli.Flag{
cli.StringFlag{
Name: "stdin",
Usage: "specify the path to the stdin fifo",
},
cli.StringFlag{
Name: "stdout",
Usage: "specify the path to the stdout fifo",
},
cli.StringFlag{
Name: "stderr",
Usage: "specify the path to the stderr fifo",
},
cli.BoolFlag{
Name: "tty,t",
Usage: "enable tty support",
},
}
// Command is the cli command for interacting with a shim
var Command = cli.Command{
Name: "shim",
Usage: "interact with a shim directly",
Flags: []cli.Flag{
cli.StringFlag{
Name: "socket",
Usage: "socket on which to connect to the shim",
},
},
Subcommands: []cli.Command{
deleteCommand,
execCommand,
startCommand,
stateCommand,
},
}
var startCommand = cli.Command{
Name: "start",
Usage: "start a container with a shim",
Action: func(context *cli.Context) error {
service, err := getShimService(context)
if err != nil {
return err
}
_, err = service.Start(gocontext.Background(), &shim.StartRequest{
ID: context.Args().First(),
})
return err
},
}
var deleteCommand = cli.Command{
Name: "delete",
Usage: "delete a container with a shim",
Action: func(context *cli.Context) error {
service, err := getShimService(context)
if err != nil {
return err
}
r, err := service.Delete(gocontext.Background(), empty)
if err != nil {
return err
}
fmt.Printf("container deleted and returned exit status %d\n", r.ExitStatus)
return nil
},
}
var stateCommand = cli.Command{
Name: "state",
Usage: "get the state of all the processes of the shim",
Action: func(context *cli.Context) error {
service, err := getShimService(context)
if err != nil {
return err
}
r, err := service.State(gocontext.Background(), &shim.StateRequest{
ID: context.Args().First(),
})
if err != nil {
return err
}
commands.PrintAsJSON(r)
return nil
},
}
var execCommand = cli.Command{
Name: "exec",
Usage: "exec a new process in the shim's container",
Flags: append(fifoFlags,
cli.BoolFlag{
Name: "attach,a",
Usage: "stay attached to the container and open the fifos",
},
cli.StringSliceFlag{
Name: "env,e",
Usage: "add environment vars",
Value: &cli.StringSlice{},
},
cli.StringFlag{
Name: "cwd",
Usage: "current working directory",
},
cli.StringFlag{
Name: "spec",
Usage: "runtime spec",
},
),
Action: func(context *cli.Context) error {
service, err := getShimService(context)
if err != nil {
return err
}
var (
id = context.Args().First()
ctx = gocontext.Background()
)
if id == "" {
return errors.New("exec id must be provided")
}
tty := context.Bool("tty")
wg, err := prepareStdio(context.String("stdin"), context.String("stdout"), context.String("stderr"), tty)
if err != nil {
return err
}
// read spec file and extract Any object
spec, err := ioutil.ReadFile(context.String("spec"))
if err != nil {
return err
}
url, err := typeurl.TypeURL(specs.Process{})
if err != nil {
return err
}
rq := &shim.ExecProcessRequest{
ID: id,
Spec: &ptypes.Any{
TypeUrl: url,
Value: spec,
},
Stdin: context.String("stdin"),
Stdout: context.String("stdout"),
Stderr: context.String("stderr"),
Terminal: tty,
}
if _, err := service.Exec(ctx, rq); err != nil {
return err
}
r, err := service.Start(ctx, &shim.StartRequest{
ID: id,
})
if err != nil {
return err
}
fmt.Printf("exec running with pid %d\n", r.Pid)
if context.Bool("attach") {
logrus.Info("attaching")
if tty {
current := console.Current()
defer current.Reset()
if err := current.SetRaw(); err != nil {
return err
}
size, err := current.Size()
if err != nil {
return err
}
if _, err := service.ResizePty(ctx, &shim.ResizePtyRequest{
ID: id,
Width: uint32(size.Width),
Height: uint32(size.Height),
}); err != nil {
return err
}
}
wg.Wait()
}
return nil
},
}
func getShimService(context *cli.Context) (shim.ShimService, error) {
bindSocket := context.GlobalString("socket")
if bindSocket == "" {
return nil, errors.New("socket path must be specified")
}
conn, err := net.Dial("unix", "\x00"+bindSocket)
if err != nil {
return nil, err
}
client := ttrpc.NewClient(conn)
// TODO(stevvooe): This actually leaks the connection. We were leaking it
// before, so may not be a huge deal.
return shim.NewShimClient(client), nil
}

View File

@@ -0,0 +1,60 @@
/*
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 commands
import (
"syscall"
"golang.org/x/sys/unix"
)
var signalMap = map[string]syscall.Signal{
"ABRT": unix.SIGABRT,
"ALRM": unix.SIGALRM,
"BUS": unix.SIGBUS,
"CHLD": unix.SIGCHLD,
"CLD": unix.SIGCLD,
"CONT": unix.SIGCONT,
"FPE": unix.SIGFPE,
"HUP": unix.SIGHUP,
"ILL": unix.SIGILL,
"INT": unix.SIGINT,
"IO": unix.SIGIO,
"IOT": unix.SIGIOT,
"KILL": unix.SIGKILL,
"PIPE": unix.SIGPIPE,
"POLL": unix.SIGPOLL,
"PROF": unix.SIGPROF,
"PWR": unix.SIGPWR,
"QUIT": unix.SIGQUIT,
"SEGV": unix.SIGSEGV,
"STKFLT": unix.SIGSTKFLT,
"STOP": unix.SIGSTOP,
"SYS": unix.SIGSYS,
"TERM": unix.SIGTERM,
"TRAP": unix.SIGTRAP,
"TSTP": unix.SIGTSTP,
"TTIN": unix.SIGTTIN,
"TTOU": unix.SIGTTOU,
"URG": unix.SIGURG,
"USR1": unix.SIGUSR1,
"USR2": unix.SIGUSR2,
"VTALRM": unix.SIGVTALRM,
"WINCH": unix.SIGWINCH,
"XCPU": unix.SIGXCPU,
"XFSZ": unix.SIGXFSZ,
}

View File

@@ -0,0 +1,58 @@
// +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 commands
import (
"syscall"
"golang.org/x/sys/unix"
)
var signalMap = map[string]syscall.Signal{
"ABRT": unix.SIGABRT,
"ALRM": unix.SIGALRM,
"BUS": unix.SIGBUS,
"CHLD": unix.SIGCHLD,
"CONT": unix.SIGCONT,
"FPE": unix.SIGFPE,
"HUP": unix.SIGHUP,
"ILL": unix.SIGILL,
"INT": unix.SIGINT,
"IO": unix.SIGIO,
"IOT": unix.SIGIOT,
"KILL": unix.SIGKILL,
"PIPE": unix.SIGPIPE,
"PROF": unix.SIGPROF,
"QUIT": unix.SIGQUIT,
"SEGV": unix.SIGSEGV,
"STOP": unix.SIGSTOP,
"SYS": unix.SIGSYS,
"TERM": unix.SIGTERM,
"TRAP": unix.SIGTRAP,
"TSTP": unix.SIGTSTP,
"TTIN": unix.SIGTTIN,
"TTOU": unix.SIGTTOU,
"URG": unix.SIGURG,
"USR1": unix.SIGUSR1,
"USR2": unix.SIGUSR2,
"VTALRM": unix.SIGVTALRM,
"WINCH": unix.SIGWINCH,
"XCPU": unix.SIGXCPU,
"XFSZ": unix.SIGXFSZ,
}

View File

@@ -0,0 +1,39 @@
/*
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 commands
import (
"syscall"
"golang.org/x/sys/windows"
)
var signalMap = map[string]syscall.Signal{
"HUP": syscall.Signal(windows.SIGHUP),
"INT": syscall.Signal(windows.SIGINT),
"QUIT": syscall.Signal(windows.SIGQUIT),
"SIGILL": syscall.Signal(windows.SIGILL),
"TRAP": syscall.Signal(windows.SIGTRAP),
"ABRT": syscall.Signal(windows.SIGABRT),
"BUS": syscall.Signal(windows.SIGBUS),
"FPE": syscall.Signal(windows.SIGFPE),
"KILL": syscall.Signal(windows.SIGKILL),
"SEGV": syscall.Signal(windows.SIGSEGV),
"PIPE": syscall.Signal(windows.SIGPIPE),
"ALRM": syscall.Signal(windows.SIGALRM),
"TERM": syscall.Signal(windows.SIGTERM),
}

View File

@@ -0,0 +1,75 @@
/*
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 commands
import (
gocontext "context"
"fmt"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"github.com/containerd/containerd"
"github.com/sirupsen/logrus"
)
type killer interface {
Kill(gocontext.Context, syscall.Signal, ...containerd.KillOpts) error
}
// ForwardAllSignals forwards signals
func ForwardAllSignals(ctx gocontext.Context, task killer) chan os.Signal {
sigc := make(chan os.Signal, 128)
signal.Notify(sigc)
go func() {
for s := range sigc {
logrus.Debug("forwarding signal ", s)
if err := task.Kill(ctx, s.(syscall.Signal)); err != nil {
logrus.WithError(err).Errorf("forward signal %s", s)
}
}
}()
return sigc
}
// StopCatch stops and closes a channel
func StopCatch(sigc chan os.Signal) {
signal.Stop(sigc)
close(sigc)
}
// ParseSignal parses a given string into a syscall.Signal
// it checks that the signal exists in the platform-appropriate signalMap
func ParseSignal(rawSignal string) (syscall.Signal, error) {
s, err := strconv.Atoi(rawSignal)
if err == nil {
sig := syscall.Signal(s)
for _, msig := range signalMap {
if sig == msig {
return sig, nil
}
}
return -1, fmt.Errorf("unknown signal %q", rawSignal)
}
signal, ok := signalMap[strings.TrimPrefix(strings.ToUpper(rawSignal), "SIG")]
if !ok {
return -1, fmt.Errorf("unknown signal %q", rawSignal)
}
return signal, nil
}

View File

@@ -0,0 +1,629 @@
/*
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 snapshots
import (
gocontext "context"
"fmt"
"io"
"os"
"strings"
"text/tabwriter"
"time"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/diff"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/mount"
"github.com/containerd/containerd/progress"
"github.com/containerd/containerd/rootfs"
"github.com/containerd/containerd/snapshots"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/urfave/cli"
)
// Command is the cli command for managing snapshots
var Command = cli.Command{
Name: "snapshots",
Aliases: []string{"snapshot"},
Usage: "manage snapshots",
Flags: commands.SnapshotterFlags,
Subcommands: cli.Commands{
commitCommand,
diffCommand,
infoCommand,
listCommand,
mountCommand,
prepareCommand,
removeCommand,
setLabelCommand,
treeCommand,
unpackCommand,
usageCommand,
viewCommand,
},
}
var listCommand = cli.Command{
Name: "list",
Aliases: []string{"ls"},
Usage: "list snapshots",
Action: func(context *cli.Context) error {
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
var (
snapshotter = client.SnapshotService(context.GlobalString("snapshotter"))
tw = tabwriter.NewWriter(os.Stdout, 1, 8, 1, ' ', 0)
)
fmt.Fprintln(tw, "KEY\tPARENT\tKIND\t")
if err := snapshotter.Walk(ctx, func(ctx gocontext.Context, info snapshots.Info) error {
fmt.Fprintf(tw, "%v\t%v\t%v\t\n",
info.Name,
info.Parent,
info.Kind)
return nil
}); err != nil {
return err
}
return tw.Flush()
},
}
var diffCommand = cli.Command{
Name: "diff",
Usage: "get the diff of two snapshots. the default second snapshot is the first snapshot's parent.",
ArgsUsage: "[flags] <idA> [<idB>]",
Flags: append([]cli.Flag{
cli.StringFlag{
Name: "media-type",
Usage: "media type to use for creating diff",
Value: ocispec.MediaTypeImageLayerGzip,
},
cli.StringFlag{
Name: "ref",
Usage: "content upload reference to use",
},
cli.BoolFlag{
Name: "keep",
Usage: "keep diff content. up to creator to delete it.",
},
}, commands.LabelFlag),
Action: func(context *cli.Context) error {
var (
idA = context.Args().First()
idB = context.Args().Get(1)
)
if idA == "" {
return errors.New("snapshot id must be provided")
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
ctx, done, err := client.WithLease(ctx)
if err != nil {
return err
}
defer done()
var desc ocispec.Descriptor
labels := commands.LabelArgs(context.StringSlice("label"))
snapshotter := client.SnapshotService(context.GlobalString("snapshotter"))
fmt.Println(context.String("media-type"))
if context.Bool("keep") {
labels["containerd.io/gc.root"] = time.Now().UTC().Format(time.RFC3339)
}
opts := []diff.Opt{
diff.WithMediaType(context.String("media-type")),
diff.WithReference(context.String("ref")),
diff.WithLabels(labels),
}
if idB == "" {
desc, err = rootfs.CreateDiff(ctx, idA, snapshotter, client.DiffService(), opts...)
if err != nil {
return err
}
} else {
var a, b []mount.Mount
ds := client.DiffService()
a, err = getMounts(ctx, idA, snapshotter)
if err != nil {
return err
}
b, err = getMounts(ctx, idB, snapshotter)
if err != nil {
return err
}
desc, err = ds.Compare(ctx, a, b, opts...)
if err != nil {
return err
}
}
ra, err := client.ContentStore().ReaderAt(ctx, desc.Digest)
if err != nil {
return err
}
_, err = io.Copy(os.Stdout, content.NewReader(ra))
return err
},
}
func getMounts(ctx gocontext.Context, id string, sn snapshots.Snapshotter) ([]mount.Mount, error) {
var mounts []mount.Mount
info, err := sn.Stat(ctx, id)
if err != nil {
return nil, err
}
if info.Kind == snapshots.KindActive {
mounts, err = sn.Mounts(ctx, id)
if err != nil {
return nil, err
}
} else {
key := fmt.Sprintf("%s-view-key", id)
mounts, err = sn.View(ctx, key, id)
if err != nil {
return nil, err
}
defer sn.Remove(ctx, key)
}
return mounts, nil
}
var usageCommand = cli.Command{
Name: "usage",
Usage: "usage snapshots",
ArgsUsage: "[flags] [<key>, ...]",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "b",
Usage: "display size in bytes",
},
},
Action: func(context *cli.Context) error {
var displaySize func(int64) string
if context.Bool("b") {
displaySize = func(s int64) string {
return fmt.Sprintf("%d", s)
}
} else {
displaySize = func(s int64) string {
return progress.Bytes(s).String()
}
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
var (
snapshotter = client.SnapshotService(context.GlobalString("snapshotter"))
tw = tabwriter.NewWriter(os.Stdout, 1, 8, 1, ' ', 0)
)
fmt.Fprintln(tw, "KEY\tSIZE\tINODES\t")
if context.NArg() == 0 {
if err := snapshotter.Walk(ctx, func(ctx gocontext.Context, info snapshots.Info) error {
usage, err := snapshotter.Usage(ctx, info.Name)
if err != nil {
return err
}
fmt.Fprintf(tw, "%v\t%s\t%d\t\n", info.Name, displaySize(usage.Size), usage.Inodes)
return nil
}); err != nil {
return err
}
} else {
for _, id := range context.Args() {
usage, err := snapshotter.Usage(ctx, id)
if err != nil {
return err
}
fmt.Fprintf(tw, "%v\t%s\t%d\t\n", id, displaySize(usage.Size), usage.Inodes)
}
}
return tw.Flush()
},
}
var removeCommand = cli.Command{
Name: "remove",
Aliases: []string{"rm"},
ArgsUsage: "<key> [<key>, ...]",
Usage: "remove snapshots",
Action: func(context *cli.Context) error {
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
snapshotter := client.SnapshotService(context.GlobalString("snapshotter"))
for _, key := range context.Args() {
err = snapshotter.Remove(ctx, key)
if err != nil {
return errors.Wrapf(err, "failed to remove %q", key)
}
}
return nil
},
}
var prepareCommand = cli.Command{
Name: "prepare",
Usage: "prepare a snapshot from a committed snapshot",
ArgsUsage: "[flags] <key> [<parent>]",
Flags: []cli.Flag{
cli.StringFlag{
Name: "target, t",
Usage: "mount target path, will print mount, if provided",
},
},
Action: func(context *cli.Context) error {
if narg := context.NArg(); narg < 1 || narg > 2 {
return cli.ShowSubcommandHelp(context)
}
var (
target = context.String("target")
key = context.Args().Get(0)
parent = context.Args().Get(1)
)
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
snapshotter := client.SnapshotService(context.GlobalString("snapshotter"))
mounts, err := snapshotter.Prepare(ctx, key, parent)
if err != nil {
return err
}
if target != "" {
printMounts(target, mounts)
}
return nil
},
}
var viewCommand = cli.Command{
Name: "view",
Usage: "create a read-only snapshot from a committed snapshot",
ArgsUsage: "[flags] <key> [<parent>]",
Flags: []cli.Flag{
cli.StringFlag{
Name: "target, t",
Usage: "mount target path, will print mount, if provided",
},
},
Action: func(context *cli.Context) error {
if narg := context.NArg(); narg < 1 || narg > 2 {
return cli.ShowSubcommandHelp(context)
}
var (
target = context.String("target")
key = context.Args().Get(0)
parent = context.Args().Get(1)
)
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
snapshotter := client.SnapshotService(context.GlobalString("snapshotter"))
mounts, err := snapshotter.View(ctx, key, parent)
if err != nil {
return err
}
if target != "" {
printMounts(target, mounts)
}
return nil
},
}
var mountCommand = cli.Command{
Name: "mounts",
Aliases: []string{"m", "mount"},
Usage: "mount gets mount commands for the snapshots",
ArgsUsage: "<target> <key>",
Action: func(context *cli.Context) error {
if context.NArg() != 2 {
return cli.ShowSubcommandHelp(context)
}
var (
target = context.Args().Get(0)
key = context.Args().Get(1)
)
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
snapshotter := client.SnapshotService(context.GlobalString("snapshotter"))
mounts, err := snapshotter.Mounts(ctx, key)
if err != nil {
return err
}
printMounts(target, mounts)
return nil
},
}
var commitCommand = cli.Command{
Name: "commit",
Usage: "commit an active snapshot into the provided name",
ArgsUsage: "<key> <active>",
Action: func(context *cli.Context) error {
if context.NArg() != 2 {
return cli.ShowSubcommandHelp(context)
}
var (
key = context.Args().Get(0)
active = context.Args().Get(1)
)
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
snapshotter := client.SnapshotService(context.GlobalString("snapshotter"))
return snapshotter.Commit(ctx, key, active)
},
}
var treeCommand = cli.Command{
Name: "tree",
Usage: "display tree view of snapshot branches",
Action: func(context *cli.Context) error {
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
var (
snapshotter = client.SnapshotService(context.GlobalString("snapshotter"))
tree = newSnapshotTree()
)
if err := snapshotter.Walk(ctx, func(ctx gocontext.Context, info snapshots.Info) error {
// Get or create node and add node details
tree.add(info)
return nil
}); err != nil {
return err
}
printTree(tree)
return nil
},
}
var infoCommand = cli.Command{
Name: "info",
Usage: "get info about a snapshot",
ArgsUsage: "<key>",
Action: func(context *cli.Context) error {
if context.NArg() != 1 {
return cli.ShowSubcommandHelp(context)
}
key := context.Args().Get(0)
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
snapshotter := client.SnapshotService(context.GlobalString("snapshotter"))
info, err := snapshotter.Stat(ctx, key)
if err != nil {
return err
}
commands.PrintAsJSON(info)
return nil
},
}
var setLabelCommand = cli.Command{
Name: "label",
Usage: "add labels to content",
ArgsUsage: "<name> [<label>=<value> ...]",
Description: "labels snapshots in the snapshotter",
Action: func(context *cli.Context) error {
key, labels := commands.ObjectWithLabelArgs(context)
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
snapshotter := client.SnapshotService(context.GlobalString("snapshotter"))
info := snapshots.Info{
Name: key,
Labels: map[string]string{},
}
var paths []string
for k, v := range labels {
paths = append(paths, fmt.Sprintf("labels.%s", k))
if v != "" {
info.Labels[k] = v
}
}
// Nothing updated, do no clear
if len(paths) == 0 {
info, err = snapshotter.Stat(ctx, info.Name)
} else {
info, err = snapshotter.Update(ctx, info, paths...)
}
if err != nil {
return err
}
var labelStrings []string
for k, v := range info.Labels {
labelStrings = append(labelStrings, fmt.Sprintf("%s=%s", k, v))
}
fmt.Println(strings.Join(labelStrings, ","))
return nil
},
}
var unpackCommand = cli.Command{
Name: "unpack",
Usage: "unpack applies layers from a manifest to a snapshot",
ArgsUsage: "[flags] <digest>",
Flags: commands.SnapshotterFlags,
Action: func(context *cli.Context) error {
dgst, err := digest.Parse(context.Args().First())
if err != nil {
return err
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
log.G(ctx).Debugf("unpacking layers from manifest %s", dgst.String())
// TODO: Support unpack by name
images, err := client.ListImages(ctx)
if err != nil {
return err
}
var unpacked bool
for _, image := range images {
if image.Target().Digest == dgst {
fmt.Printf("unpacking %s (%s)...", dgst, image.Target().MediaType)
if err := image.Unpack(ctx, context.String("snapshotter")); err != nil {
fmt.Println()
return err
}
fmt.Println("done")
unpacked = true
break
}
}
if !unpacked {
return errors.New("manifest not found")
}
// TODO: Get rootfs from Image
//log.G(ctx).Infof("chain ID: %s", chainID.String())
return nil
},
}
type snapshotTree struct {
nodes []*snapshotTreeNode
index map[string]*snapshotTreeNode
}
func newSnapshotTree() *snapshotTree {
return &snapshotTree{
index: make(map[string]*snapshotTreeNode),
}
}
type snapshotTreeNode struct {
info snapshots.Info
children []string
}
func (st *snapshotTree) add(info snapshots.Info) *snapshotTreeNode {
entry, ok := st.index[info.Name]
if !ok {
entry = &snapshotTreeNode{info: info}
st.nodes = append(st.nodes, entry)
st.index[info.Name] = entry
} else {
entry.info = info // update info if we created placeholder
}
if info.Parent != "" {
pn := st.get(info.Parent)
if pn == nil {
// create a placeholder
pn = st.add(snapshots.Info{Name: info.Parent})
}
pn.children = append(pn.children, info.Name)
}
return entry
}
func (st *snapshotTree) get(name string) *snapshotTreeNode {
return st.index[name]
}
func printTree(st *snapshotTree) {
for _, node := range st.nodes {
// Print for root(parent-less) nodes only
if node.info.Parent == "" {
printNode(node.info.Name, st, 0)
}
}
}
func printNode(name string, tree *snapshotTree, level int) {
node := tree.index[name]
prefix := strings.Repeat(" ", level)
if level > 0 {
prefix += "\\_"
}
fmt.Printf(prefix+" %s\n", node.info.Name)
level++
for _, child := range node.children {
printNode(child, tree, level)
}
}
func printMounts(target string, mounts []mount.Mount) {
// FIXME: This is specific to Unix
for _, m := range mounts {
fmt.Printf("mount -t %s %s %s -o %s\n", m.Type, m.Source, target, strings.Join(m.Options, ","))
}
}

View File

@@ -0,0 +1,86 @@
/*
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 tasks
import (
"github.com/containerd/console"
"github.com/containerd/containerd/cio"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var attachCommand = cli.Command{
Name: "attach",
Usage: "attach to the IO of a running container",
ArgsUsage: "CONTAINER",
Action: func(context *cli.Context) error {
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
container, err := client.LoadContainer(ctx, context.Args().First())
if err != nil {
return err
}
spec, err := container.Spec(ctx)
if err != nil {
return err
}
var (
con console.Console
tty = spec.Process.Terminal
)
if tty {
con = console.Current()
defer con.Reset()
if err := con.SetRaw(); err != nil {
return err
}
}
task, err := container.Task(ctx, cio.NewAttach(cio.WithStdio))
if err != nil {
return err
}
defer task.Delete(ctx)
statusC, err := task.Wait(ctx)
if err != nil {
return err
}
if tty {
if err := HandleConsoleResize(ctx, task, con); err != nil {
logrus.WithError(err).Error("console resize")
}
} else {
sigc := commands.ForwardAllSignals(ctx, task)
defer commands.StopCatch(sigc)
}
ec := <-statusC
code, _, err := ec.Result()
if err != nil {
return err
}
if code != 0 {
return cli.NewExitError("", int(code))
}
return nil
},
}

View File

@@ -0,0 +1,67 @@
/*
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 tasks
import (
"fmt"
"github.com/containerd/containerd"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/pkg/errors"
"github.com/urfave/cli"
)
var checkpointCommand = cli.Command{
Name: "checkpoint",
Usage: "checkpoint a container",
ArgsUsage: "[flags] CONTAINER",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "exit",
Usage: "stop the container after the checkpoint",
},
},
Action: func(context *cli.Context) error {
id := context.Args().First()
if id == "" {
return errors.New("container id must be provided")
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
container, err := client.LoadContainer(ctx, id)
if err != nil {
return err
}
task, err := container.Task(ctx, nil)
if err != nil {
return err
}
var opts []containerd.CheckpointTaskOpts
if context.Bool("exit") {
opts = append(opts, containerd.WithExit)
}
checkpoint, err := task.Checkpoint(ctx, opts...)
if err != nil {
return err
}
fmt.Println(checkpoint.Name())
return nil
},
}

View File

@@ -0,0 +1,63 @@
/*
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 tasks
import (
"github.com/containerd/containerd"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/urfave/cli"
)
var deleteCommand = cli.Command{
Name: "delete",
Usage: "[flags] delete a task",
ArgsUsage: "CONTAINER",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "force, f",
Usage: "force delete task process",
},
},
Action: func(context *cli.Context) error {
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
container, err := client.LoadContainer(ctx, context.Args().First())
if err != nil {
return err
}
task, err := container.Task(ctx, nil)
if err != nil {
return err
}
var opts []containerd.ProcessDeleteOpts
if context.Bool("force") {
opts = append(opts, containerd.WithProcessKill)
}
status, err := task.Delete(ctx, opts...)
if err != nil {
return err
}
if ec := status.ExitCode(); ec != 0 {
return cli.NewExitError("", int(ec))
}
return nil
},
}

View File

@@ -0,0 +1,129 @@
/*
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 tasks
import (
"errors"
"github.com/containerd/console"
"github.com/containerd/containerd/cio"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
//TODO:(jessvalarezo) exec-id is optional here, update to required arg
var execCommand = cli.Command{
Name: "exec",
Usage: "execute additional processes in an existing container",
ArgsUsage: "[flags] CONTAINER CMD [ARG...]",
Flags: []cli.Flag{
cli.StringFlag{
Name: "cwd",
Usage: "working directory of the new process",
},
cli.BoolFlag{
Name: "tty,t",
Usage: "allocate a TTY for the container",
},
cli.StringFlag{
Name: "exec-id",
Usage: "exec specific id for the process",
},
cli.StringFlag{
Name: "fifo-dir",
Usage: "directory used for storing IO FIFOs",
},
},
Action: func(context *cli.Context) error {
var (
id = context.Args().First()
args = context.Args().Tail()
tty = context.Bool("tty")
)
if id == "" {
return errors.New("container id must be provided")
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
container, err := client.LoadContainer(ctx, id)
if err != nil {
return err
}
spec, err := container.Spec(ctx)
if err != nil {
return err
}
task, err := container.Task(ctx, nil)
if err != nil {
return err
}
pspec := spec.Process
pspec.Terminal = tty
pspec.Args = args
cioOpts := []cio.Opt{cio.WithStdio, cio.WithFIFODir(context.String("fifo-dir"))}
if tty {
cioOpts = append(cioOpts, cio.WithTerminal)
}
ioCreator := cio.NewCreator(cioOpts...)
process, err := task.Exec(ctx, context.String("exec-id"), pspec, ioCreator)
if err != nil {
return err
}
defer process.Delete(ctx)
statusC, err := process.Wait(ctx)
if err != nil {
return err
}
var con console.Console
if tty {
con = console.Current()
defer con.Reset()
if err := con.SetRaw(); err != nil {
return err
}
}
if tty {
if err := HandleConsoleResize(ctx, process, con); err != nil {
logrus.WithError(err).Error("console resize")
}
} else {
sigc := commands.ForwardAllSignals(ctx, process)
defer commands.StopCatch(sigc)
}
if err := process.Start(ctx); err != nil {
return err
}
status := <-statusC
code, _, err := status.Result()
if err != nil {
return err
}
if code != 0 {
return cli.NewExitError("", int(code))
}
return nil
},
}

View File

@@ -0,0 +1,83 @@
/*
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 tasks
import (
"github.com/containerd/containerd"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/pkg/errors"
"github.com/urfave/cli"
)
var killCommand = cli.Command{
Name: "kill",
Usage: "signal a container (default: SIGTERM)",
ArgsUsage: "[flags] CONTAINER",
Flags: []cli.Flag{
cli.StringFlag{
Name: "signal, s",
Value: "SIGTERM",
Usage: "signal to send to the container",
},
cli.StringFlag{
Name: "exec-id",
Usage: "process ID to kill",
},
cli.BoolFlag{
Name: "all, a",
Usage: "send signal to all processes inside the container",
},
},
Action: func(context *cli.Context) error {
id := context.Args().First()
if id == "" {
return errors.New("container id must be provided")
}
signal, err := commands.ParseSignal(context.String("signal"))
if err != nil {
return err
}
var (
all = context.Bool("all")
execID = context.String("exec-id")
opts []containerd.KillOpts
)
if all && execID != "" {
return errors.New("specify an exec-id or all; not both")
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
if all {
opts = append(opts, containerd.WithKillAll)
}
if execID != "" {
opts = append(opts, containerd.WithKillExecID(execID))
}
container, err := client.LoadContainer(ctx, id)
if err != nil {
return err
}
task, err := container.Task(ctx, nil)
if err != nil {
return err
}
return task.Kill(ctx, signal, opts...)
},
}

View File

@@ -0,0 +1,71 @@
/*
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 tasks
import (
"fmt"
"os"
"text/tabwriter"
tasks "github.com/containerd/containerd/api/services/tasks/v1"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/urfave/cli"
)
var listCommand = cli.Command{
Name: "list",
Usage: "list tasks",
Aliases: []string{"ls"},
ArgsUsage: "[flags]",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "quiet, q",
Usage: "print only the task id & pid",
},
},
Action: func(context *cli.Context) error {
quiet := context.Bool("quiet")
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
s := client.TaskService()
response, err := s.List(ctx, &tasks.ListTasksRequest{})
if err != nil {
return err
}
if quiet {
for _, task := range response.Tasks {
fmt.Println(task.ID)
}
return nil
}
w := tabwriter.NewWriter(os.Stdout, 4, 8, 4, ' ', 0)
fmt.Fprintln(w, "TASK\tPID\tSTATUS\t")
for _, task := range response.Tasks {
if _, err := fmt.Fprintf(w, "%s\t%d\t%s\n",
task.ID,
task.Pid,
task.Status.String(),
); err != nil {
return err
}
}
return w.Flush()
},
}

View File

@@ -0,0 +1,44 @@
/*
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 tasks
import (
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/urfave/cli"
)
var pauseCommand = cli.Command{
Name: "pause",
Usage: "pause an existing container",
ArgsUsage: "CONTAINER",
Action: func(context *cli.Context) error {
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
container, err := client.LoadContainer(ctx, context.Args().First())
if err != nil {
return err
}
task, err := container.Task(ctx, nil)
if err != nil {
return err
}
return task.Pause(ctx)
},
}

View File

@@ -0,0 +1,72 @@
/*
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 tasks
import (
"fmt"
"os"
"text/tabwriter"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/typeurl"
"github.com/pkg/errors"
"github.com/urfave/cli"
)
var psCommand = cli.Command{
Name: "ps",
Usage: "list processes for container",
ArgsUsage: "CONTAINER",
Action: func(context *cli.Context) error {
id := context.Args().First()
if id == "" {
return errors.New("container id must be provided")
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
container, err := client.LoadContainer(ctx, id)
if err != nil {
return err
}
task, err := container.Task(ctx, nil)
if err != nil {
return err
}
processes, err := task.Pids(ctx)
if err != nil {
return err
}
w := tabwriter.NewWriter(os.Stdout, 1, 8, 4, ' ', 0)
fmt.Fprintln(w, "PID\tINFO")
for _, ps := range processes {
var info interface{} = "-"
if ps.Info != nil {
info, err = typeurl.UnmarshalAny(ps.Info)
if err != nil {
return err
}
}
if _, err := fmt.Fprintf(w, "%d\t%+v\n", ps.Pid, info); err != nil {
return err
}
}
return w.Flush()
},
}

View File

@@ -0,0 +1,44 @@
/*
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 tasks
import (
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/urfave/cli"
)
var resumeCommand = cli.Command{
Name: "resume",
Usage: "resume a paused container",
ArgsUsage: "CONTAINER",
Action: func(context *cli.Context) error {
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
container, err := client.LoadContainer(ctx, context.Args().First())
if err != nil {
return err
}
task, err := container.Task(ctx, nil)
if err != nil {
return err
}
return task.Resume(ctx)
},
}

View File

@@ -0,0 +1,122 @@
/*
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 tasks
import (
"github.com/containerd/console"
"github.com/containerd/containerd/cio"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
)
var startCommand = cli.Command{
Name: "start",
Usage: "start a container that have been created",
ArgsUsage: "CONTAINER",
Flags: []cli.Flag{
cli.BoolFlag{
Name: "null-io",
Usage: "send all IO to /dev/null",
},
cli.StringFlag{
Name: "fifo-dir",
Usage: "directory used for storing IO FIFOs",
},
cli.StringFlag{
Name: "pid-file",
Usage: "file path to write the task's pid",
},
},
Action: func(context *cli.Context) error {
var (
err error
id = context.Args().Get(0)
)
if id == "" {
return errors.New("container id must be provided")
}
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
container, err := client.LoadContainer(ctx, id)
if err != nil {
return err
}
spec, err := container.Spec(ctx)
if err != nil {
return err
}
var (
tty = spec.Process.Terminal
opts = getNewTaskOpts(context)
ioOpts = []cio.Opt{cio.WithFIFODir(context.String("fifo-dir"))}
)
task, err := NewTask(ctx, client, container, "", tty, context.Bool("null-io"), ioOpts, opts...)
if err != nil {
return err
}
defer task.Delete(ctx)
if context.IsSet("pid-file") {
if err := commands.WritePidFile(context.String("pid-file"), int(task.Pid())); err != nil {
return err
}
}
statusC, err := task.Wait(ctx)
if err != nil {
return err
}
var con console.Console
if tty {
con = console.Current()
defer con.Reset()
if err := con.SetRaw(); err != nil {
return err
}
}
if err := task.Start(ctx); err != nil {
return err
}
if tty {
if err := HandleConsoleResize(ctx, task, con); err != nil {
logrus.WithError(err).Error("console resize")
}
} else {
sigc := commands.ForwardAllSignals(ctx, task)
defer commands.StopCatch(sigc)
}
status := <-statusC
code, _, err := status.Result()
if err != nil {
return err
}
if _, err := task.Delete(ctx); err != nil {
return err
}
if code != 0 {
return cli.NewExitError("", int(code))
}
return nil
},
}

View File

@@ -0,0 +1,46 @@
/*
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 tasks
import (
gocontext "context"
"github.com/urfave/cli"
)
type resizer interface {
Resize(ctx gocontext.Context, w, h uint32) error
}
// Command is the cli command for managing tasks
var Command = cli.Command{
Name: "tasks",
Usage: "manage tasks",
Aliases: []string{"t", "task"},
Subcommands: []cli.Command{
attachCommand,
checkpointCommand,
deleteCommand,
execCommand,
listCommand,
killCommand,
pauseCommand,
psCommand,
resumeCommand,
startCommand,
},
}

View File

@@ -0,0 +1,98 @@
// +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 tasks
import (
gocontext "context"
"os"
"os/signal"
"github.com/containerd/console"
"github.com/containerd/containerd"
"github.com/containerd/containerd/cio"
"github.com/containerd/containerd/log"
"github.com/pkg/errors"
"github.com/urfave/cli"
"golang.org/x/sys/unix"
)
func init() {
startCommand.Flags = append(startCommand.Flags, cli.BoolFlag{
Name: "no-pivot",
Usage: "disable use of pivot-root (linux only)",
})
}
// HandleConsoleResize resizes the console
func HandleConsoleResize(ctx gocontext.Context, task resizer, con console.Console) error {
// do an initial resize of the console
size, err := con.Size()
if err != nil {
return err
}
if err := task.Resize(ctx, uint32(size.Width), uint32(size.Height)); err != nil {
log.G(ctx).WithError(err).Error("resize pty")
}
s := make(chan os.Signal, 16)
signal.Notify(s, unix.SIGWINCH)
go func() {
for range s {
size, err := con.Size()
if err != nil {
log.G(ctx).WithError(err).Error("get pty size")
continue
}
if err := task.Resize(ctx, uint32(size.Width), uint32(size.Height)); err != nil {
log.G(ctx).WithError(err).Error("resize pty")
}
}
}()
return nil
}
// NewTask creates a new task
func NewTask(ctx gocontext.Context, client *containerd.Client, container containerd.Container, checkpoint string, tty, nullIO bool, ioOpts []cio.Opt, opts ...containerd.NewTaskOpts) (containerd.Task, error) {
stdio := cio.NewCreator(append([]cio.Opt{cio.WithStdio}, ioOpts...)...)
if checkpoint == "" {
ioCreator := stdio
if tty {
ioCreator = cio.NewCreator(append([]cio.Opt{cio.WithStdio, cio.WithTerminal}, ioOpts...)...)
}
if nullIO {
if tty {
return nil, errors.New("tty and null-io cannot be used together")
}
ioCreator = cio.NullIO
}
return container.NewTask(ctx, ioCreator, opts...)
}
im, err := client.GetImage(ctx, checkpoint)
if err != nil {
return nil, err
}
opts = append(opts, containerd.WithTaskCheckpoint(im))
return container.NewTask(ctx, stdio, opts...)
}
func getNewTaskOpts(context *cli.Context) []containerd.NewTaskOpts {
if context.Bool("no-pivot") {
return []containerd.NewTaskOpts{containerd.WithNoPivotRoot}
}
return nil
}

View File

@@ -0,0 +1,77 @@
/*
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 tasks
import (
gocontext "context"
"time"
"github.com/containerd/console"
"github.com/containerd/containerd"
"github.com/containerd/containerd/cio"
"github.com/containerd/containerd/log"
"github.com/pkg/errors"
"github.com/urfave/cli"
)
// HandleConsoleResize resizes the console
func HandleConsoleResize(ctx gocontext.Context, task resizer, con console.Console) error {
// do an initial resize of the console
size, err := con.Size()
if err != nil {
return err
}
go func() {
prevSize := size
for {
time.Sleep(time.Millisecond * 250)
size, err := con.Size()
if err != nil {
log.G(ctx).WithError(err).Error("get pty size")
continue
}
if size.Width != prevSize.Width || size.Height != prevSize.Height {
if err := task.Resize(ctx, uint32(size.Width), uint32(size.Height)); err != nil {
log.G(ctx).WithError(err).Error("resize pty")
}
prevSize = size
}
}
}()
return nil
}
// NewTask creates a new task
func NewTask(ctx gocontext.Context, client *containerd.Client, container containerd.Container, _ string, tty, nullIO bool, ioOpts []cio.Opt, opts ...containerd.NewTaskOpts) (containerd.Task, error) {
ioCreator := cio.NewCreator(append([]cio.Opt{cio.WithStdio}, ioOpts...)...)
if tty {
ioCreator = cio.NewCreator(append([]cio.Opt{cio.WithStdio, cio.WithTerminal}, ioOpts...)...)
}
if nullIO {
if tty {
return nil, errors.New("tty and null-io cannot be used together")
}
ioCreator = cio.NullIO
}
return container.NewTask(ctx, ioCreator)
}
func getNewTaskOpts(_ *cli.Context) []containerd.NewTaskOpts {
return nil
}

View File

@@ -0,0 +1,57 @@
/*
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 version
import (
"fmt"
"os"
"github.com/containerd/containerd/cmd/ctr/commands"
"github.com/containerd/containerd/version"
"github.com/urfave/cli"
)
// Command is a cli command to output the client and containerd server version
var Command = cli.Command{
Name: "version",
Usage: "print the client and server versions",
Action: func(context *cli.Context) error {
fmt.Println("Client:")
fmt.Println(" Version: ", version.Version)
fmt.Println(" Revision:", version.Revision)
fmt.Println("")
client, ctx, cancel, err := commands.NewClient(context)
if err != nil {
return err
}
defer cancel()
v, err := client.Version(ctx)
if err != nil {
return err
}
fmt.Println("Server:")
fmt.Println(" Version: ", v.Version)
fmt.Println(" Revision:", v.Revision)
if v.Version != version.Version {
fmt.Fprintln(os.Stderr, "WARNING: version mismatch")
}
if v.Revision != version.Revision {
fmt.Fprintln(os.Stderr, "WARNING: revision mismatch")
}
return nil
},
}

View File

@@ -78,7 +78,7 @@ func WriteBlob(ctx context.Context, cs Ingester, ref string, r io.Reader, size i
}
// Copy copies data with the expected digest from the reader into the
// provided content store writer.
// provided content store writer. This copy commits the writer.
//
// This is useful when the digest and size are known beforehand. When
// the size or digest is unknown, these values may be empty.
@@ -113,6 +113,22 @@ func Copy(ctx context.Context, cw Writer, r io.Reader, size int64, expected dige
return nil
}
// CopyReaderAt copies to a writer from a given reader at for the given
// number of bytes. This copy does not commit the writer.
func CopyReaderAt(cw Writer, ra ReaderAt, n int64) error {
ws, err := cw.Status()
if err != nil {
return err
}
buf := bufPool.Get().(*[]byte)
defer bufPool.Put(buf)
_, err = io.CopyBuffer(cw, io.NewSectionReader(ra, ws.Offset, n), *buf)
return err
}
// seekReader attempts to seek the reader to the given offset, either by
// resolving `io.Seeker`, by detecting `io.ReaderAt`, or discarding
// up to the given offset.

View File

@@ -182,9 +182,9 @@ func SetChildrenLabels(manager content.Manager, f HandlerFunc) HandlerFunc {
}
}
// FilterPlatform is a handler wrapper which limits the descriptors returned
// by a handler to a single platform.
func FilterPlatform(platform string, f HandlerFunc) HandlerFunc {
// FilterPlatforms is a handler wrapper which limits the descriptors returned
// by a handler to the specified platforms.
func FilterPlatforms(f HandlerFunc, platformList ...string) HandlerFunc {
return func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
children, err := f(ctx, desc)
if err != nil {
@@ -192,32 +192,25 @@ func FilterPlatform(platform string, f HandlerFunc) HandlerFunc {
}
var descs []ocispec.Descriptor
if platform != "" && isMultiPlatform(desc.MediaType) {
p, err := platforms.Parse(platform)
if err != nil {
return nil, err
}
matcher := platforms.NewMatcher(p)
for _, d := range children {
if d.Platform == nil || matcher.Match(*d.Platform) {
descs = append(descs, d)
if len(platformList) == 0 {
descs = children
} else {
for _, platform := range platformList {
p, err := platforms.Parse(platform)
if err != nil {
return nil, err
}
matcher := platforms.NewMatcher(p)
for _, d := range children {
if d.Platform == nil || matcher.Match(*d.Platform) {
descs = append(descs, d)
}
}
}
} else {
descs = children
}
return descs, nil
}
}
func isMultiPlatform(mediaType string) bool {
switch mediaType {
case MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
return true
default:
return false
}
}

View File

@@ -118,7 +118,7 @@ func (image *Image) Size(ctx context.Context, provider content.Provider, platfor
}
size += desc.Size
return nil, nil
}), FilterPlatform(platform, ChildrenHandler(provider))), image.Target)
}), FilterPlatforms(ChildrenHandler(provider), platform)), image.Target)
}
// Manifest resolves a manifest from the image for the given platform.

View File

@@ -0,0 +1,204 @@
/*
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 oci
import (
"archive/tar"
"context"
"encoding/json"
"io"
"sort"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/platforms"
ocispecs "github.com/opencontainers/image-spec/specs-go"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
// V1Exporter implements OCI Image Spec v1.
// It is up to caller to put "org.opencontainers.image.ref.name" annotation to desc.
//
// TODO(AkihiroSuda): add V1Exporter{TranslateMediaTypes: true} that transforms media types,
// e.g. application/vnd.docker.image.rootfs.diff.tar.gzip
// -> application/vnd.oci.image.layer.v1.tar+gzip
type V1Exporter struct {
}
// Export implements Exporter.
func (oe *V1Exporter) Export(ctx context.Context, store content.Provider, desc ocispec.Descriptor, writer io.Writer) error {
tw := tar.NewWriter(writer)
defer tw.Close()
records := []tarRecord{
ociLayoutFile(""),
ociIndexRecord(desc),
}
algorithms := map[string]struct{}{}
exportHandler := func(ctx context.Context, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
records = append(records, blobRecord(store, desc))
algorithms[desc.Digest.Algorithm().String()] = struct{}{}
return nil, nil
}
handlers := images.Handlers(
images.FilterPlatforms(images.ChildrenHandler(store), platforms.Default()),
images.HandlerFunc(exportHandler),
)
// Walk sequentially since the number of fetchs is likely one and doing in
// parallel requires locking the export handler
if err := images.Walk(ctx, handlers, desc); err != nil {
return err
}
if len(algorithms) > 0 {
records = append(records, directoryRecord("blobs/", 0755))
for alg := range algorithms {
records = append(records, directoryRecord("blobs/"+alg+"/", 0755))
}
}
return writeTar(ctx, tw, records)
}
type tarRecord struct {
Header *tar.Header
CopyTo func(context.Context, io.Writer) (int64, error)
}
func blobRecord(cs content.Provider, desc ocispec.Descriptor) tarRecord {
path := "blobs/" + desc.Digest.Algorithm().String() + "/" + desc.Digest.Hex()
return tarRecord{
Header: &tar.Header{
Name: path,
Mode: 0444,
Size: desc.Size,
Typeflag: tar.TypeReg,
},
CopyTo: func(ctx context.Context, w io.Writer) (int64, error) {
r, err := cs.ReaderAt(ctx, desc.Digest)
if err != nil {
return 0, err
}
defer r.Close()
// Verify digest
dgstr := desc.Digest.Algorithm().Digester()
n, err := io.Copy(io.MultiWriter(w, dgstr.Hash()), content.NewReader(r))
if err != nil {
return 0, err
}
if dgstr.Digest() != desc.Digest {
return 0, errors.Errorf("unexpected digest %s copied", dgstr.Digest())
}
return n, nil
},
}
}
func directoryRecord(name string, mode int64) tarRecord {
return tarRecord{
Header: &tar.Header{
Name: name,
Mode: mode,
Typeflag: tar.TypeDir,
},
}
}
func ociLayoutFile(version string) tarRecord {
if version == "" {
version = ocispec.ImageLayoutVersion
}
layout := ocispec.ImageLayout{
Version: version,
}
b, err := json.Marshal(layout)
if err != nil {
panic(err)
}
return tarRecord{
Header: &tar.Header{
Name: ocispec.ImageLayoutFile,
Mode: 0444,
Size: int64(len(b)),
Typeflag: tar.TypeReg,
},
CopyTo: func(ctx context.Context, w io.Writer) (int64, error) {
n, err := w.Write(b)
return int64(n), err
},
}
}
func ociIndexRecord(manifests ...ocispec.Descriptor) tarRecord {
index := ocispec.Index{
Versioned: ocispecs.Versioned{
SchemaVersion: 2,
},
Manifests: manifests,
}
b, err := json.Marshal(index)
if err != nil {
panic(err)
}
return tarRecord{
Header: &tar.Header{
Name: "index.json",
Mode: 0644,
Size: int64(len(b)),
Typeflag: tar.TypeReg,
},
CopyTo: func(ctx context.Context, w io.Writer) (int64, error) {
n, err := w.Write(b)
return int64(n), err
},
}
}
func writeTar(ctx context.Context, tw *tar.Writer, records []tarRecord) error {
sort.Slice(records, func(i, j int) bool {
return records[i].Header.Name < records[j].Header.Name
})
for _, record := range records {
if err := tw.WriteHeader(record.Header); err != nil {
return err
}
if record.CopyTo != nil {
n, err := record.CopyTo(ctx, tw)
if err != nil {
return err
}
if n != record.Header.Size {
return errors.Errorf("unexpected copy size for %s", record.Header.Name)
}
} else if record.Header.Size > 0 {
return errors.Errorf("no content to write to record with non-zero size for %s", record.Header.Name)
}
}
return nil
}

View File

@@ -0,0 +1,204 @@
/*
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 oci provides the importer and the exporter for OCI Image Spec.
package oci
import (
"archive/tar"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"path"
"strings"
"github.com/containerd/containerd/content"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images"
digest "github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
)
// V1Importer implements OCI Image Spec v1.
type V1Importer struct {
// ImageName is preprended to either `:` + OCI ref name or `@` + digest (for anonymous refs).
// This field is mandatory atm, but may change in the future. maybe ref map[string]string as in moby/moby#33355
ImageName string
}
var _ images.Importer = &V1Importer{}
// Import implements Importer.
func (oi *V1Importer) Import(ctx context.Context, store content.Store, reader io.Reader) ([]images.Image, error) {
if oi.ImageName == "" {
return nil, errors.New("ImageName not set")
}
tr := tar.NewReader(reader)
var imgrecs []images.Image
foundIndexJSON := false
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return nil, err
}
if hdr.Typeflag != tar.TypeReg && hdr.Typeflag != tar.TypeRegA {
continue
}
hdrName := path.Clean(hdr.Name)
if hdrName == "index.json" {
if foundIndexJSON {
return nil, errors.New("duplicated index.json")
}
foundIndexJSON = true
imgrecs, err = onUntarIndexJSON(tr, oi.ImageName)
if err != nil {
return nil, err
}
continue
}
if strings.HasPrefix(hdrName, "blobs/") {
if err := onUntarBlob(ctx, tr, store, hdrName, hdr.Size); err != nil {
return nil, err
}
}
}
if !foundIndexJSON {
return nil, errors.New("no index.json found")
}
for _, img := range imgrecs {
err := setGCRefContentLabels(ctx, store, img.Target)
if err != nil {
return imgrecs, err
}
}
// FIXME(AkihiroSuda): set GC labels for unreferrenced blobs (i.e. with unknown media types)?
return imgrecs, nil
}
func onUntarIndexJSON(r io.Reader, imageName string) ([]images.Image, error) {
b, err := ioutil.ReadAll(r)
if err != nil {
return nil, err
}
var idx ocispec.Index
if err := json.Unmarshal(b, &idx); err != nil {
return nil, err
}
var imgrecs []images.Image
for _, m := range idx.Manifests {
ref, err := normalizeImageRef(imageName, m)
if err != nil {
return nil, err
}
imgrecs = append(imgrecs, images.Image{
Name: ref,
Target: m,
})
}
return imgrecs, nil
}
func normalizeImageRef(imageName string, manifest ocispec.Descriptor) (string, error) {
digest := manifest.Digest
if digest == "" {
return "", errors.Errorf("manifest with empty digest: %v", manifest)
}
ociRef := manifest.Annotations[ocispec.AnnotationRefName]
if ociRef == "" {
return imageName + "@" + digest.String(), nil
}
return imageName + ":" + ociRef, nil
}
func onUntarBlob(ctx context.Context, r io.Reader, store content.Ingester, name string, size int64) error {
// name is like "blobs/sha256/deadbeef"
split := strings.Split(name, "/")
if len(split) != 3 {
return errors.Errorf("unexpected name: %q", name)
}
algo := digest.Algorithm(split[1])
if !algo.Available() {
return errors.Errorf("unsupported algorithm: %s", algo)
}
dgst := digest.NewDigestFromHex(algo.String(), split[2])
return content.WriteBlob(ctx, store, "unknown-"+dgst.String(), r, size, dgst)
}
// GetChildrenDescriptors returns children blob descriptors for the following supported types:
// - images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest
// - images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex
func GetChildrenDescriptors(r io.Reader, desc ocispec.Descriptor) ([]ocispec.Descriptor, error) {
switch desc.MediaType {
case images.MediaTypeDockerSchema2Manifest, ocispec.MediaTypeImageManifest:
var manifest ocispec.Manifest
if err := json.NewDecoder(r).Decode(&manifest); err != nil {
return nil, err
}
return append([]ocispec.Descriptor{manifest.Config}, manifest.Layers...), nil
case images.MediaTypeDockerSchema2ManifestList, ocispec.MediaTypeImageIndex:
var index ocispec.Index
if err := json.NewDecoder(r).Decode(&index); err != nil {
return nil, err
}
return index.Manifests, nil
}
return nil, nil
}
func setGCRefContentLabels(ctx context.Context, store content.Store, desc ocispec.Descriptor) error {
info, err := store.Info(ctx, desc.Digest)
if err != nil {
if errdefs.IsNotFound(err) {
// when the archive is created from multi-arch image,
// it may contain only blobs for a certain platform.
// So ErrNotFound (on manifest list) is expected here.
return nil
}
return err
}
ra, err := store.ReaderAt(ctx, desc.Digest)
if err != nil {
return err
}
defer ra.Close()
r := content.NewReader(ra)
children, err := GetChildrenDescriptors(r, desc)
if err != nil {
return err
}
if info.Labels == nil {
info.Labels = map[string]string{}
}
for i, child := range children {
// Note: child blob is not guaranteed to be written to the content store. (multi-arch)
info.Labels[fmt.Sprintf("containerd.io/gc.ref.content.%d", i)] = child.Digest.String()
}
if _, err := store.Update(ctx, info, "labels"); err != nil {
return err
}
for _, child := range children {
if err := setGCRefContentLabels(ctx, store, child); err != nil {
return err
}
}
return nil
}

View File

@@ -52,7 +52,7 @@ var (
bucketKeyObjectSnapshots = []byte("snapshots") // stores snapshot references
bucketKeyObjectContent = []byte("content") // stores content references
bucketKeyObjectBlob = []byte("blob") // stores content links
bucketKeyObjectIngest = []byte("ingest") // stores ingest links
bucketKeyObjectIngests = []byte("ingests") // stores ingest objects
bucketKeyObjectLeases = []byte("leases") // stores leases
bucketKeyDigest = []byte("digest")
@@ -70,6 +70,10 @@ var (
bucketKeyTarget = []byte("target")
bucketKeyExtensions = []byte("extensions")
bucketKeyCreatedAt = []byte("createdat")
bucketKeyExpected = []byte("expected")
bucketKeyRef = []byte("ref")
deprecatedBucketKeyObjectIngest = []byte("ingest") // stores ingest links, deprecated in v1.2
)
func getBucket(tx *bolt.Tx, keys ...[]byte) *bolt.Bucket {
@@ -178,14 +182,18 @@ func getBlobBucket(tx *bolt.Tx, namespace string, dgst digest.Digest) *bolt.Buck
return getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectContent, bucketKeyObjectBlob, []byte(dgst.String()))
}
func createIngestBucket(tx *bolt.Tx, namespace string) (*bolt.Bucket, error) {
bkt, err := createBucketIfNotExists(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectContent, bucketKeyObjectIngest)
func getIngestsBucket(tx *bolt.Tx, namespace string) *bolt.Bucket {
return getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectContent, bucketKeyObjectIngests)
}
func createIngestBucket(tx *bolt.Tx, namespace, ref string) (*bolt.Bucket, error) {
bkt, err := createBucketIfNotExists(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectContent, bucketKeyObjectIngests, []byte(ref))
if err != nil {
return nil, err
}
return bkt, nil
}
func getIngestBucket(tx *bolt.Tx, namespace string) *bolt.Bucket {
return getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectContent, bucketKeyObjectIngest)
func getIngestBucket(tx *bolt.Tx, namespace, ref string) *bolt.Bucket {
return getBucket(tx, bucketKeyVersion, []byte(namespace), bucketKeyObjectContent, bucketKeyObjectIngests, []byte(ref))
}

View File

@@ -226,14 +226,16 @@ func (cs *contentStore) ListStatuses(ctx context.Context, fs ...string) ([]conte
brefs := map[string]string{}
if err := view(ctx, cs.db, func(tx *bolt.Tx) error {
bkt := getIngestBucket(tx, ns)
bkt := getIngestsBucket(tx, ns)
if bkt == nil {
return nil
}
return bkt.ForEach(func(k, v []byte) error {
// TODO(dmcgowan): match name and potentially labels here
brefs[string(k)] = string(v)
if v == nil {
// TODO(dmcgowan): match name and potentially labels here
brefs[string(k)] = string(bkt.Bucket(k).Get(bucketKeyRef))
}
return nil
})
}); err != nil {
@@ -261,11 +263,11 @@ func (cs *contentStore) ListStatuses(ctx context.Context, fs ...string) ([]conte
}
func getRef(tx *bolt.Tx, ns, ref string) string {
bkt := getIngestBucket(tx, ns)
bkt := getIngestBucket(tx, ns, ref)
if bkt == nil {
return ""
}
v := bkt.Get([]byte(ref))
v := bkt.Get(bucketKeyRef)
if len(v) == 0 {
return ""
}
@@ -308,19 +310,29 @@ func (cs *contentStore) Abort(ctx context.Context, ref string) error {
defer cs.l.RUnlock()
return update(ctx, cs.db, func(tx *bolt.Tx) error {
bkt := getIngestBucket(tx, ns)
ibkt := getIngestsBucket(tx, ns)
if ibkt == nil {
return errors.Wrapf(errdefs.ErrNotFound, "reference %v", ref)
}
bkt := ibkt.Bucket([]byte(ref))
if bkt == nil {
return errors.Wrapf(errdefs.ErrNotFound, "reference %v", ref)
}
bref := string(bkt.Get([]byte(ref)))
bref := string(bkt.Get(bucketKeyRef))
if bref == "" {
return errors.Wrapf(errdefs.ErrNotFound, "reference %v", ref)
}
if err := bkt.Delete([]byte(ref)); err != nil {
expected := string(bkt.Get(bucketKeyExpected))
if err := ibkt.DeleteBucket([]byte(ref)); err != nil {
return err
}
return cs.Store.Abort(ctx, bref)
// if not shared content, delete active ingest on backend
if expected == "" {
return cs.Store.Abort(ctx, bref)
}
return nil
})
}
@@ -337,8 +349,10 @@ func (cs *contentStore) Writer(ctx context.Context, ref string, size int64, expe
var (
w content.Writer
exists bool
bref string
)
if err := update(ctx, cs.db, func(tx *bolt.Tx) error {
var shared bool
if expected != "" {
cbkt := getBlobBucket(tx, ns, expected)
if cbkt != nil {
@@ -352,18 +366,24 @@ func (cs *contentStore) Writer(ctx context.Context, ref string, size int64, expe
exists = true
return nil
}
if st, err := cs.Store.Info(ctx, expected); err == nil {
// Ensure the expected size is the same, it is likely
// an error if the size is mismatched but the caller
// must resolve this on commit
if size == 0 || size == st.Size {
shared = true
size = st.Size
}
}
}
bkt, err := createIngestBucket(tx, ns)
bkt, err := createIngestBucket(tx, ns, ref)
if err != nil {
return err
}
var (
bref string
brefb = bkt.Get([]byte(ref))
)
brefb := bkt.Get(bucketKeyRef)
if brefb == nil {
sid, err := bkt.NextSequence()
if err != nil {
@@ -371,21 +391,24 @@ func (cs *contentStore) Writer(ctx context.Context, ref string, size int64, expe
}
bref = createKey(sid, ns, ref)
if err := bkt.Put([]byte(ref), []byte(bref)); err != nil {
if err := bkt.Put(bucketKeyRef, []byte(bref)); err != nil {
return err
}
} else {
bref = string(brefb)
}
// Do not use the passed in expected value here since it was
// already checked against the user metadata. If the content
// store has the content, it must still be written before
// linked into the given namespace. It is possible in the future
// to allow content which exists in content store but not
// namespace to be linked here and returned an exist error, but
// this would require more configuration to make secure.
w, err = cs.Store.Writer(ctx, bref, size, "")
if shared {
if err := bkt.Put(bucketKeyExpected, []byte(expected)); err != nil {
return err
}
} else {
// Do not use the passed in expected value here since it was
// already checked against the user metadata. The content must
// be committed in the namespace before it will be seen as
// available in the current namespace.
w, err = cs.Store.Writer(ctx, bref, size, "")
}
return err
}); err != nil {
return nil, err
@@ -394,23 +417,99 @@ func (cs *contentStore) Writer(ctx context.Context, ref string, size int64, expe
return nil, errors.Wrapf(errdefs.ErrAlreadyExists, "content %v", expected)
}
// TODO: keep the expected in the writer to use on commit
// when no expected is provided there.
return &namespacedWriter{
Writer: w,
ctx: ctx,
ref: ref,
namespace: ns,
db: cs.db,
provider: cs.Store,
l: &cs.l,
w: w,
bref: bref,
started: time.Now(),
expected: expected,
size: size,
}, nil
}
type namespacedWriter struct {
content.Writer
ctx context.Context
ref string
namespace string
db transactor
l *sync.RWMutex
provider interface {
content.Provider
content.Ingester
}
l *sync.RWMutex
w content.Writer
bref string
started time.Time
expected digest.Digest
size int64
}
func (nw *namespacedWriter) Close() error {
if nw.w != nil {
return nw.w.Close()
}
return nil
}
func (nw *namespacedWriter) Write(p []byte) (int, error) {
// if no writer, first copy and unshare before performing write
if nw.w == nil {
if len(p) == 0 {
return 0, nil
}
if err := nw.createAndCopy(nw.ctx, nw.size); err != nil {
return 0, err
}
}
return nw.w.Write(p)
}
func (nw *namespacedWriter) Digest() digest.Digest {
if nw.w != nil {
return nw.w.Digest()
}
return nw.expected
}
func (nw *namespacedWriter) Truncate(size int64) error {
if nw.w != nil {
return nw.w.Truncate(size)
}
return nw.createAndCopy(nw.ctx, size)
}
func (nw *namespacedWriter) createAndCopy(ctx context.Context, size int64) error {
w, err := nw.provider.Writer(ctx, nw.bref, nw.size, "")
if err != nil {
return err
}
if size > 0 {
ra, err := nw.provider.ReaderAt(ctx, nw.expected)
if err != nil {
return err
}
defer ra.Close()
if err := content.CopyReaderAt(w, ra, size); err != nil {
nw.w.Close()
nw.w = nil
return err
}
}
nw.w = w
return nil
}
func (nw *namespacedWriter) Commit(ctx context.Context, size int64, expected digest.Digest, opts ...content.Opt) error {
@@ -418,9 +517,9 @@ func (nw *namespacedWriter) Commit(ctx context.Context, size int64, expected dig
defer nw.l.RUnlock()
return update(ctx, nw.db, func(tx *bolt.Tx) error {
bkt := getIngestBucket(tx, nw.namespace)
bkt := getIngestsBucket(tx, nw.namespace)
if bkt != nil {
if err := bkt.Delete([]byte(nw.ref)); err != nil {
if err := bkt.DeleteBucket([]byte(nw.ref)); err != nil && err != bolt.ErrBucketNotFound {
return err
}
}
@@ -443,24 +542,38 @@ func (nw *namespacedWriter) commit(ctx context.Context, tx *bolt.Tx, size int64,
return "", err
}
status, err := nw.Writer.Status()
if err != nil {
return "", err
}
if size != 0 && size != status.Offset {
return "", errors.Errorf("%q failed size validation: %v != %v", nw.ref, status.Offset, size)
}
size = status.Offset
actual := nw.Writer.Digest()
if err := nw.Writer.Commit(ctx, size, expected); err != nil {
if !errdefs.IsAlreadyExists(err) {
return "", err
var actual digest.Digest
if nw.w == nil {
if size != 0 && size != nw.size {
return "", errors.Errorf("%q failed size validation: %v != %v", nw.ref, nw.size, size)
}
if expected != "" && expected != nw.expected {
return "", errors.Errorf("%q unexpected digest", nw.ref)
}
size = nw.size
actual = nw.expected
if getBlobBucket(tx, nw.namespace, actual) != nil {
return "", errors.Wrapf(errdefs.ErrAlreadyExists, "content %v", actual)
}
} else {
status, err := nw.w.Status()
if err != nil {
return "", err
}
if size != 0 && size != status.Offset {
return "", errors.Errorf("%q failed size validation: %v != %v", nw.ref, status.Offset, size)
}
size = status.Offset
actual = nw.w.Digest()
if err := nw.w.Commit(ctx, size, expected); err != nil {
if !errdefs.IsAlreadyExists(err) {
return "", err
}
if getBlobBucket(tx, nw.namespace, actual) != nil {
return "", errors.Wrapf(errdefs.ErrAlreadyExists, "content %v", actual)
}
}
}
bkt, err := createBlobBucket(tx, nw.namespace, actual)
@@ -484,12 +597,20 @@ func (nw *namespacedWriter) commit(ctx context.Context, tx *bolt.Tx, size int64,
return actual, bkt.Put(bucketKeySize, sizeEncoded)
}
func (nw *namespacedWriter) Status() (content.Status, error) {
st, err := nw.Writer.Status()
func (nw *namespacedWriter) Status() (st content.Status, err error) {
if nw.w != nil {
st, err = nw.w.Status()
} else {
st.Offset = nw.size
st.Total = nw.size
st.StartedAt = nw.started
st.UpdatedAt = nw.started
st.Expected = nw.expected
}
if err == nil {
st.Ref = nw.ref
}
return st, err
return
}
func (cs *contentStore) ReaderAt(ctx context.Context, dgst digest.Digest) (content.ReaderAt, error) {
@@ -590,13 +711,30 @@ func (cs *contentStore) garbageCollect(ctx context.Context) (d time.Duration, er
continue
}
bbkt := cbkt.Bucket(bucketKeyObjectBlob)
if err := bbkt.ForEach(func(ck, cv []byte) error {
if cv == nil {
seen[string(ck)] = struct{}{}
if bbkt != nil {
if err := bbkt.ForEach(func(ck, cv []byte) error {
if cv == nil {
seen[string(ck)] = struct{}{}
}
return nil
}); err != nil {
return err
}
}
ibkt := cbkt.Bucket(bucketKeyObjectIngests)
if ibkt != nil {
if err := ibkt.ForEach(func(ref, v []byte) error {
if v == nil {
expected := ibkt.Bucket(ref).Get(bucketKeyExpected)
if len(expected) > 0 {
seen[string(expected)] = struct{}{}
}
}
return nil
}); err != nil {
return err
}
return nil
}); err != nil {
return err
}
}

View File

@@ -43,7 +43,7 @@ const (
// dbVersion represents updates to the schema
// version which are additions and compatible with
// prior version of the same schema.
dbVersion = 1
dbVersion = 2
)
// DB represents a metadata database backed by a bolt

View File

@@ -40,6 +40,11 @@ var migrations = []migration{
version: 1,
migrate: addChildLinks,
},
{
schema: "v1",
version: 2,
migrate: migrateIngests,
},
}
// addChildLinks Adds children key to the snapshotters to enforce snapshot
@@ -99,3 +104,53 @@ func addChildLinks(tx *bolt.Tx) error {
return nil
}
// migrateIngests moves ingests from the key/value ingest bucket
// to a structured ingest bucket for storing additional state about
// an ingest.
func migrateIngests(tx *bolt.Tx) error {
v1bkt := tx.Bucket(bucketKeyVersion)
if v1bkt == nil {
return nil
}
// iterate through each namespace
v1c := v1bkt.Cursor()
for k, v := v1c.First(); k != nil; k, v = v1c.Next() {
if v != nil {
continue
}
bkt := v1bkt.Bucket(k).Bucket(bucketKeyObjectContent)
if bkt == nil {
continue
}
dbkt := bkt.Bucket(deprecatedBucketKeyObjectIngest)
if dbkt == nil {
continue
}
// Create new ingests bucket
nbkt, err := bkt.CreateBucketIfNotExists(bucketKeyObjectIngests)
if err != nil {
return err
}
if err := dbkt.ForEach(func(ref, bref []byte) error {
ibkt, err := nbkt.CreateBucketIfNotExists(ref)
if err != nil {
return err
}
return ibkt.Put(bucketKeyRef, bref)
}); err != nil {
return err
}
if err := bkt.DeleteBucket(deprecatedBucketKeyObjectIngest); err != nil {
return err
}
}
return nil
}

View File

@@ -613,14 +613,23 @@ func validateSnapshot(info *snapshots.Info) error {
return nil
}
type cleaner interface {
Cleanup(ctx context.Context) error
}
func (s *snapshotter) garbageCollect(ctx context.Context) (d time.Duration, err error) {
s.l.Lock()
t1 := time.Now()
defer func() {
s.l.Unlock()
if err == nil {
if c, ok := s.Snapshotter.(cleaner); ok {
err = c.Cleanup(ctx)
}
}
if err == nil {
d = time.Now().Sub(t1)
}
s.l.Unlock()
}()
seen := map[string]struct{}{}

View File

@@ -17,11 +17,11 @@
package namespaces
import (
"context"
"os"
"github.com/containerd/containerd/errdefs"
"github.com/pkg/errors"
"golang.org/x/net/context"
)
const (

View File

@@ -0,0 +1,82 @@
/*
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 progress
import (
"bytes"
"fmt"
)
// TODO(stevvooe): We may want to support more interesting parameterization of
// the bar. For now, it is very simple.
// Bar provides a very simple progress bar implementation.
//
// Use with fmt.Printf and "r" to format the progress bar. A "-" flag makes it
// progress from right to left.
type Bar float64
var _ fmt.Formatter = Bar(1.0)
// Format the progress bar as output
func (h Bar) Format(state fmt.State, r rune) {
switch r {
case 'r':
default:
panic(fmt.Sprintf("%v: unexpected format character", float64(h)))
}
if h > 1.0 {
h = 1.0
}
if h < 0.0 {
h = 0.0
}
if state.Flag('-') {
h = 1.0 - h
}
width, ok := state.Width()
if !ok {
// default width of 40
width = 40
}
var pad int
extra := len([]byte(green)) + len([]byte(reset))
p := make([]byte, width+extra)
p[0], p[len(p)-1] = '|', '|'
pad += 2
positive := int(Bar(width-pad) * h)
negative := width - pad - positive
n := 1
n += copy(p[n:], []byte(green))
n += copy(p[n:], bytes.Repeat([]byte("+"), positive))
n += copy(p[n:], []byte(reset))
if negative > 0 {
copy(p[n:len(p)-1], bytes.Repeat([]byte("-"), negative))
}
state.Write(p)
}

View File

@@ -0,0 +1,18 @@
/*
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 progress assists in displaying human readable progress information.
package progress

View 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 progress
const (
escape = "\x1b"
reset = escape + "[0m"
red = escape + "[31m" // nolint: unused, varcheck
green = escape + "[32m"
)

View File

@@ -0,0 +1,45 @@
/*
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 progress
import (
"fmt"
"time"
units "github.com/docker/go-units"
)
// Bytes converts a regular int64 to human readable type.
type Bytes int64
// String returns the string representation of bytes
func (b Bytes) String() string {
return units.CustomSize("%02.1f %s", float64(b), 1024.0, []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"})
}
// BytesPerSecond is the rate in seconds for byte operations
type BytesPerSecond int64
// NewBytesPerSecond returns the rate that n bytes were written in the provided duration
func NewBytesPerSecond(n int64, duration time.Duration) BytesPerSecond {
return BytesPerSecond(float64(n) / duration.Seconds())
}
// String returns the string representation of the rate
func (bps BytesPerSecond) String() string {
return fmt.Sprintf("%v/s", Bytes(bps))
}

View File

@@ -0,0 +1,115 @@
/*
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 progress
import (
"bytes"
"fmt"
"io"
"os"
"regexp"
"strings"
"github.com/containerd/console"
)
var (
regexCleanLine = regexp.MustCompile("\x1b\\[[0-9]+m[\x1b]?")
)
// Writer buffers writes until flush, at which time the last screen is cleared
// and the current buffer contents are written. This is useful for
// implementing progress displays, such as those implemented in docker and
// git.
type Writer struct {
buf bytes.Buffer
w io.Writer
lines int
}
// NewWriter returns a writer
func NewWriter(w io.Writer) *Writer {
return &Writer{
w: w,
}
}
// Write the provided bytes
func (w *Writer) Write(p []byte) (n int, err error) {
return w.buf.Write(p)
}
// Flush should be called when refreshing the current display.
func (w *Writer) Flush() error {
if w.buf.Len() == 0 {
return nil
}
if err := w.clearLines(); err != nil {
return err
}
w.lines = countLines(w.buf.String())
if _, err := w.w.Write(w.buf.Bytes()); err != nil {
return err
}
w.buf.Reset()
return nil
}
// TODO(stevvooe): The following are system specific. Break these out if we
// decide to build this package further.
func (w *Writer) clearLines() error {
for i := 0; i < w.lines; i++ {
if _, err := fmt.Fprintf(w.w, "\x1b[1A\x1b[2K\r"); err != nil {
return err
}
}
return nil
}
// countLines in the output. If a line is longer than the console width then
// an extra line is added to the count for each wrapped line. If the console
// width is undefined then 0 is returned so that no lines are cleared on the next
// flush.
func countLines(output string) int {
con, err := console.ConsoleFromFile(os.Stdin)
if err != nil {
return 0
}
ws, err := con.Size()
if err != nil {
return 0
}
width := int(ws.Width)
if width <= 0 {
return 0
}
strlines := strings.Split(output, "\n")
lines := -1
for _, line := range strlines {
lines += (len(stripLine(line))-1)/width + 1
}
return lines
}
func stripLine(line string) string {
return string(regexCleanLine.ReplaceAll([]byte(line), []byte{}))
}

View File

@@ -29,7 +29,6 @@ import (
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/images"
"github.com/containerd/containerd/log"
"github.com/containerd/containerd/platforms"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@@ -182,7 +181,7 @@ func push(ctx context.Context, provider content.Provider, pusher Pusher, desc oc
//
// Base handlers can be provided which will be called before any push specific
// handlers.
func PushContent(ctx context.Context, pusher Pusher, desc ocispec.Descriptor, provider content.Provider, baseHandlers ...images.Handler) error {
func PushContent(ctx context.Context, pusher Pusher, desc ocispec.Descriptor, provider content.Provider, platforms []string, baseHandlers ...images.Handler) error {
var m sync.Mutex
manifestStack := []ocispec.Descriptor{}
@@ -202,7 +201,7 @@ func PushContent(ctx context.Context, pusher Pusher, desc ocispec.Descriptor, pr
pushHandler := PushHandler(pusher, provider)
handlers := append(baseHandlers,
images.FilterPlatform(platforms.Default(), images.ChildrenHandler(provider)),
images.FilterPlatforms(images.ChildrenHandler(provider), platforms...),
filterHandler,
pushHandler,
)

View File

@@ -366,7 +366,7 @@ func (s *service) Write(session api.Content_WriteServer) (err error) {
// users use the same writer style for each with a minimum of overhead.
if req.Expected != "" {
if expected != "" && expected != req.Expected {
return status.Errorf(codes.InvalidArgument, "inconsistent digest provided: %v != %v", req.Expected, expected)
log.G(ctx).Debugf("commit digest differs from writer digest: %v != %v", req.Expected, expected)
}
expected = req.Expected
@@ -383,7 +383,7 @@ func (s *service) Write(session api.Content_WriteServer) (err error) {
// Update the expected total. Typically, this could be seen at
// negotiation time or on a commit message.
if total > 0 && req.Total != total {
return status.Errorf(codes.InvalidArgument, "inconsistent total provided: %v != %v", req.Total, total)
log.G(ctx).Debugf("commit size differs from writer size: %v != %v", req.Total, total)
}
total = req.Total
}

View File

@@ -44,20 +44,45 @@ func init() {
InitFn: func(ic *plugin.InitContext) (interface{}, error) {
ic.Meta.Platforms = append(ic.Meta.Platforms, platforms.DefaultSpec())
ic.Meta.Exports["root"] = ic.Root
return NewSnapshotter(ic.Root)
return NewSnapshotter(ic.Root, AsynchronousRemove)
},
})
}
// SnapshotterConfig is used to configure the overlay snapshotter instance
type SnapshotterConfig struct {
asyncRemove bool
}
// Opt is an option to configure the overlay snapshotter
type Opt func(config *SnapshotterConfig) error
// AsynchronousRemove defers removal of filesystem content until
// the Cleanup method is called. Removals will make the snapshot
// referred to by the key unavailable and make the key immediately
// available for re-use.
func AsynchronousRemove(config *SnapshotterConfig) error {
config.asyncRemove = true
return nil
}
type snapshotter struct {
root string
ms *storage.MetaStore
root string
ms *storage.MetaStore
asyncRemove bool
}
// NewSnapshotter returns a Snapshotter which uses overlayfs. The overlayfs
// diffs are stored under the provided root. A metadata file is stored under
// the root.
func NewSnapshotter(root string) (snapshots.Snapshotter, error) {
func NewSnapshotter(root string, opts ...Opt) (snapshots.Snapshotter, error) {
var config SnapshotterConfig
for _, opt := range opts {
if err := opt(&config); err != nil {
return nil, err
}
}
if err := os.MkdirAll(root, 0700); err != nil {
return nil, err
}
@@ -78,8 +103,9 @@ func NewSnapshotter(root string) (snapshots.Snapshotter, error) {
}
return &snapshotter{
root: root,
ms: ms,
root: root,
ms: ms,
asyncRemove: config.asyncRemove,
}, nil
}
@@ -210,47 +236,50 @@ func (o *snapshotter) Commit(ctx context.Context, name, key string, opts ...snap
return t.Commit()
}
// Remove abandons the transaction identified by key. All resources
// associated with the key will be removed.
// Remove abandons the snapshot identified by key. The snapshot will
// immediately become unavailable and unrecoverable. Disk space will
// be freed up on the next call to `Cleanup`.
func (o *snapshotter) Remove(ctx context.Context, key string) (err error) {
ctx, t, err := o.ms.TransactionContext(ctx, true)
if err != nil {
return err
}
defer func() {
if err != nil && t != nil {
if err != nil {
if rerr := t.Rollback(); rerr != nil {
log.G(ctx).WithError(rerr).Warn("failed to rollback transaction")
}
}
}()
id, _, err := storage.Remove(ctx, key)
_, _, err = storage.Remove(ctx, key)
if err != nil {
return errors.Wrap(err, "failed to remove")
}
path := filepath.Join(o.root, "snapshots", id)
renamed := filepath.Join(o.root, "snapshots", "rm-"+id)
if err := os.Rename(path, renamed); err != nil {
return errors.Wrap(err, "failed to rename")
}
err = t.Commit()
t = nil
if err != nil {
if err1 := os.Rename(renamed, path); err1 != nil {
// May cause inconsistent data on disk
log.G(ctx).WithError(err1).WithField("path", renamed).Errorf("failed to rename after failed commit")
if !o.asyncRemove {
var removals []string
removals, err = o.getCleanupDirectories(ctx, t)
if err != nil {
return errors.Wrap(err, "unable to get directories for removal")
}
return errors.Wrap(err, "failed to commit")
}
if err := os.RemoveAll(renamed); err != nil {
// Must be cleaned up, any "rm-*" could be removed if no active transactions
log.G(ctx).WithError(err).WithField("path", renamed).Warnf("failed to remove root filesystem")
// Remove directories after the transaction is closed, failures must not
// return error since the transaction is committed with the removal
// key no longer available.
defer func() {
if err == nil {
for _, dir := range removals {
if err := os.RemoveAll(dir); err != nil {
log.G(ctx).WithError(err).WithField("path", dir).Warn("failed to remove directory")
}
}
}
}()
}
return nil
return t.Commit()
}
// Walk the committed snapshots.
@@ -263,45 +292,94 @@ func (o *snapshotter) Walk(ctx context.Context, fn func(context.Context, snapsho
return storage.WalkInfo(ctx, fn)
}
func (o *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt) ([]mount.Mount, error) {
var (
path string
snapshotDir = filepath.Join(o.root, "snapshots")
)
td, err := ioutil.TempDir(snapshotDir, "new-")
// Cleanup cleans up disk resources from removed or abandoned snapshots
func (o *snapshotter) Cleanup(ctx context.Context) error {
cleanup, err := o.cleanupDirectories(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to create temp dir")
return err
}
for _, dir := range cleanup {
if err := os.RemoveAll(dir); err != nil {
log.G(ctx).WithError(err).WithField("path", dir).Warn("failed to remove directory")
}
}
return nil
}
func (o *snapshotter) cleanupDirectories(ctx context.Context) ([]string, error) {
// Get a write transaction to ensure no other write transaction can be entered
// while the cleanup is scanning.
ctx, t, err := o.ms.TransactionContext(ctx, true)
if err != nil {
return nil, err
}
defer t.Rollback()
return o.getCleanupDirectories(ctx, t)
}
func (o *snapshotter) getCleanupDirectories(ctx context.Context, t storage.Transactor) ([]string, error) {
ids, err := storage.IDMap(ctx)
if err != nil {
return nil, err
}
snapshotDir := filepath.Join(o.root, "snapshots")
fd, err := os.Open(snapshotDir)
if err != nil {
return nil, err
}
defer fd.Close()
dirs, err := fd.Readdirnames(0)
if err != nil {
return nil, err
}
cleanup := []string{}
for _, d := range dirs {
if _, ok := ids[d]; ok {
continue
}
cleanup = append(cleanup, filepath.Join(snapshotDir, d))
}
return cleanup, nil
}
func (o *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt) ([]mount.Mount, error) {
ctx, t, err := o.ms.TransactionContext(ctx, true)
if err != nil {
return nil, err
}
var td, path string
defer func() {
if err != nil {
if td != "" {
if err1 := os.RemoveAll(td); err1 != nil {
err = errors.Wrapf(err, "remove failed: %v", err1)
log.G(ctx).WithError(err1).Warn("failed to cleanup temp snapshot directory")
}
}
if path != "" {
if err1 := os.RemoveAll(path); err1 != nil {
log.G(ctx).WithError(err1).WithField("path", path).Error("failed to reclaim snapshot directory, directory may need removal")
err = errors.Wrapf(err, "failed to remove path: %v", err1)
}
}
}
}()
fs := filepath.Join(td, "fs")
if err = os.MkdirAll(fs, 0755); err != nil {
return nil, err
}
if kind == snapshots.KindActive {
if err = os.MkdirAll(filepath.Join(td, "work"), 0711); err != nil {
return nil, err
}
}
ctx, t, err := o.ms.TransactionContext(ctx, true)
snapshotDir := filepath.Join(o.root, "snapshots")
td, err = o.prepareDirectory(ctx, snapshotDir, kind)
if err != nil {
return nil, err
if rerr := t.Rollback(); rerr != nil {
log.G(ctx).WithError(rerr).Warn("failed to rollback transaction")
}
return nil, errors.Wrap(err, "failed to create prepare snapshot dir")
}
rollback := true
defer func() {
@@ -324,7 +402,11 @@ func (o *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, k
}
stat := st.Sys().(*syscall.Stat_t)
if err := os.Lchown(fs, int(stat.Uid), int(stat.Gid)); err != nil {
if err := os.Lchown(filepath.Join(td, "fs"), int(stat.Uid), int(stat.Gid)); err != nil {
if rerr := t.Rollback(); rerr != nil {
log.G(ctx).WithError(rerr).Warn("failed to rollback transaction")
}
return nil, errors.Wrap(err, "failed to chown")
}
}
@@ -343,6 +425,25 @@ func (o *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, k
return o.mounts(s), nil
}
func (o *snapshotter) prepareDirectory(ctx context.Context, snapshotDir string, kind snapshots.Kind) (string, error) {
td, err := ioutil.TempDir(filepath.Join(o.root, "snapshots"), "new-")
if err != nil {
return "", errors.Wrap(err, "failed to create temp dir")
}
if err := os.Mkdir(filepath.Join(td, "fs"), 0755); err != nil {
return td, err
}
if kind == snapshots.KindActive {
if err := os.Mkdir(filepath.Join(td, "work"), 0711); err != nil {
return td, err
}
}
return td, nil
}
func (o *snapshotter) mounts(s storage.Snapshot) []mount.Mount {
if len(s.ParentIDs) == 0 {
// if we only have one layer/no parents then just return a bind mount as overlay

View File

@@ -405,6 +405,26 @@ func CommitActive(ctx context.Context, key, name string, usage snapshots.Usage,
return fmt.Sprintf("%d", id), nil
}
// IDMap returns all the IDs mapped to their key
func IDMap(ctx context.Context) (map[string]string, error) {
m := map[string]string{}
if err := withBucket(ctx, func(ctx context.Context, bkt, _ *bolt.Bucket) error {
return bkt.ForEach(func(k, v []byte) error {
// skip non buckets
if v != nil {
return nil
}
id := readID(bkt.Bucket(k))
m[fmt.Sprintf("%d", id)] = string(k)
return nil
})
}); err != nil {
return nil, err
}
return m, nil
}
func withSnapshotBucket(ctx context.Context, key string, fn func(context.Context, *bolt.Bucket, *bolt.Bucket) error) error {
tx, ok := ctx.Value(transactionKey{}).(*bolt.Tx)
if !ok {

View File

@@ -41,8 +41,9 @@ github.com/stevvooe/ttrpc d4528379866b0ce7e9d71f3eb96f0582fc374577
github.com/syndtr/gocapability db04d3cc01c8b54962a58ec7e491717d06cfcc16
github.com/gotestyourself/gotestyourself 44dbf532bbf5767611f6f2a61bded572e337010a
github.com/google/go-cmp v0.1.0
# cri dependencies
github.com/containerd/cri-containerd dcc278739fb31c5369f927c69716275c084c3d53 https://github.com/Random-Liu/cri-containerd.git
github.com/containerd/cri 0c876040681ebe8a291fa2cebefdcc2796fa3fc8
github.com/blang/semver v3.1.0
github.com/containernetworking/cni v0.6.0
github.com/containernetworking/plugins v0.6.0
@@ -73,3 +74,8 @@ k8s.io/apiserver 8e45eac9dff86447a5c2effe6a3d2cba70121ebf
k8s.io/client-go 33bd23f75b6de861994706a322b0afab824b2171
k8s.io/kubernetes 05944b1d2ca7f60b09762a330425108f48f6b603
k8s.io/utils 258e2a2fa64568210fbd6267cf1d8fd87c3cb86e
# zfs dependencies
github.com/containerd/zfs 2e6f60521b5690bf2f265c416a42b251c2a3ec8e
github.com/mistifyio/go-zfs 166add352731e515512690329794ee593f1aaff2
github.com/pborman/uuid c65b2f87fee37d1c7854c9164a450713c28d50cd