diff --git a/Makefile b/Makefile index 352928fd1..6d26db67b 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ INTEGRATION_PACKAGE=${PKG} TEST_REQUIRES_ROOT_PACKAGES=$(shell for f in $$(git grep -l testutil.RequiresRoot | grep -v Makefile);do echo "${PKG}/$$(dirname $$f)"; done) # Project binaries. -COMMANDS=ctr containerd protoc-gen-gogoctrd dist +COMMANDS=ctr containerd protoc-gen-gogoctrd ifneq ("$(GOOS)", "windows") COMMANDS += containerd-shim endif diff --git a/cmd/dist/apply.go b/cmd/ctr/apply.go similarity index 100% rename from cmd/dist/apply.go rename to cmd/ctr/apply.go diff --git a/cmd/dist/fetch.go b/cmd/ctr/fetch.go similarity index 99% rename from cmd/dist/fetch.go rename to cmd/ctr/fetch.go index d59abcc8d..cc76a643d 100644 --- a/cmd/dist/fetch.go +++ b/cmd/ctr/fetch.go @@ -54,7 +54,7 @@ Most of this is experimental and there are few leaps to make this work.`, } func fetch(ctx context.Context, ref string, clicontext *cli.Context) (containerd.Image, error) { - client, err := getClient(clicontext) + client, err := newClient(clicontext) if err != nil { return nil, err } diff --git a/cmd/dist/fetchobject.go b/cmd/ctr/fetchobject.go similarity index 100% rename from cmd/dist/fetchobject.go rename to cmd/ctr/fetchobject.go diff --git a/cmd/ctr/main.go b/cmd/ctr/main.go index 8427dc553..f3d281d90 100644 --- a/cmd/ctr/main.go +++ b/cmd/ctr/main.go @@ -59,6 +59,11 @@ containerd CLI } app.Commands = append([]cli.Command{ imageCommand, + pullCommand, + fetchCommand, + fetchObjectCommand, + pushCommand, + pushObjectCommand, containersCommand, checkpointCommand, runCommand, @@ -76,6 +81,8 @@ containerd CLI snapshotCommand, versionCommand, psCommand, + applyCommand, + rootfsCommand, }, extraCmds...) app.Before = func(context *cli.Context) error { if context.GlobalBool("debug") { diff --git a/cmd/dist/pull.go b/cmd/ctr/pull.go similarity index 100% rename from cmd/dist/pull.go rename to cmd/ctr/pull.go diff --git a/cmd/dist/push.go b/cmd/ctr/push.go similarity index 99% rename from cmd/dist/push.go rename to cmd/ctr/push.go index ef6dbe36c..ffc1549c8 100644 --- a/cmd/dist/push.go +++ b/cmd/ctr/push.go @@ -55,7 +55,7 @@ var pushCommand = cli.Command{ ctx, cancel := appContext(clicontext) defer cancel() - client, err := getClient(clicontext) + client, err := newClient(clicontext) if err != nil { return err } diff --git a/cmd/dist/pushobject.go b/cmd/ctr/pushobject.go similarity index 97% rename from cmd/dist/pushobject.go rename to cmd/ctr/pushobject.go index fab26b8fa..989c6c93c 100644 --- a/cmd/dist/pushobject.go +++ b/cmd/ctr/pushobject.go @@ -43,7 +43,7 @@ var pushObjectCommand = cli.Command{ return err } - cs, err := resolveContentStore(clicontext) + cs, err := getContentStore(clicontext) if err != nil { return err } diff --git a/cmd/dist/rootfs.go b/cmd/ctr/rootfs.go similarity index 97% rename from cmd/dist/rootfs.go rename to cmd/ctr/rootfs.go index a4250c88b..c266adc53 100644 --- a/cmd/dist/rootfs.go +++ b/cmd/ctr/rootfs.go @@ -33,7 +33,7 @@ var rootfsUnpackCommand = cli.Command{ log.G(ctx).Debugf("unpacking layers from manifest %s", dgst.String()) - client, err := getClient(clicontext) + client, err := newClient(clicontext) if err != nil { return err } diff --git a/cmd/ctr/utils.go b/cmd/ctr/utils.go index 14502860d..b20aef7af 100644 --- a/cmd/ctr/utils.go +++ b/cmd/ctr/utils.go @@ -1,16 +1,23 @@ package main import ( + "bufio" gocontext "context" + "crypto/tls" "encoding/csv" + "encoding/json" "fmt" + "net" + "net/http" "os" "os/signal" "strconv" "strings" "syscall" + "time" "github.com/Sirupsen/logrus" + "github.com/containerd/console" "github.com/containerd/containerd" containersapi "github.com/containerd/containerd/api/services/containers/v1" contentapi "github.com/containerd/containerd/api/services/content/v1" @@ -24,23 +31,49 @@ import ( "github.com/containerd/containerd/content" "github.com/containerd/containerd/images" "github.com/containerd/containerd/namespaces" + "github.com/containerd/containerd/remotes" + "github.com/containerd/containerd/remotes/docker" + "github.com/containerd/containerd/rootfs" contentservice "github.com/containerd/containerd/services/content" "github.com/containerd/containerd/services/diff" imagesservice "github.com/containerd/containerd/services/images" namespacesservice "github.com/containerd/containerd/services/namespaces" snapshotservice "github.com/containerd/containerd/services/snapshot" "github.com/containerd/containerd/snapshot" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" specs "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" "github.com/urfave/cli" "google.golang.org/grpc" ) -var snapshotterFlags = []cli.Flag{ - cli.StringFlag{ - Name: "snapshotter", - Usage: "Snapshotter name. Empty value stands for the daemon default value.", - }, -} +var ( + snapshotterFlags = []cli.Flag{ + cli.StringFlag{ + Name: "snapshotter", + Usage: "Snapshotter name. Empty value stands for the daemon default value.", + }, + } + + 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", + }, + } +) var grpcConn *grpc.ClientConn @@ -145,6 +178,110 @@ func getVersionService(context *cli.Context) (versionservice.VersionClient, erro return versionservice.NewVersionClient(conn), nil } +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 +} + +func getImageLayers(ctx gocontext.Context, image images.Image, cs content.Store) ([]rootfs.Layer, error) { + p, err := content.ReadBlob(ctx, cs, image.Target.Digest) + if err != nil { + return nil, errors.Wrapf(err, "failed to read manifest blob") + } + + var manifest ocispec.Manifest + if err := json.Unmarshal(p, &manifest); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal manifest") + } + + diffIDs, err := image.RootFS(ctx, cs) + if err != nil { + return nil, errors.Wrap(err, "failed to resolve rootfs") + } + + if len(diffIDs) != len(manifest.Layers) { + return nil, errors.Errorf("mismatched image rootfs and manifest layers") + } + + layers := make([]rootfs.Layer, len(diffIDs)) + for i := range diffIDs { + layers[i].Diff = ocispec.Descriptor{ + // TODO: derive media type from compressed type + MediaType: ocispec.MediaTypeImageLayer, + Digest: diffIDs[i], + } + layers[i].Blob = manifest.Layers[i] + } + + return layers, 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 +} + func forwardAllSignals(ctx gocontext.Context, task killer) chan os.Signal { sigc := make(chan os.Signal, 128) signal.Notify(sigc) diff --git a/cmd/dist/common.go b/cmd/dist/common.go deleted file mode 100644 index 28e9c04be..000000000 --- a/cmd/dist/common.go +++ /dev/null @@ -1,195 +0,0 @@ -package main - -import ( - "bufio" - "context" - contextpkg "context" - "crypto/tls" - "encoding/json" - "fmt" - "net" - "net/http" - "strings" - "time" - - "github.com/containerd/console" - "github.com/containerd/containerd" - "github.com/containerd/containerd/content" - "github.com/containerd/containerd/images" - "github.com/containerd/containerd/namespaces" - "github.com/containerd/containerd/remotes" - "github.com/containerd/containerd/remotes/docker" - "github.com/containerd/containerd/rootfs" - ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/pkg/errors" - "github.com/urfave/cli" -) - -var 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", - }, -} - -var snapshotterFlags = []cli.Flag{ - cli.StringFlag{ - Name: "snapshotter", - Usage: "Snapshotter name. Empty value stands for the daemon default value.", - }, -} - -func getClient(context *cli.Context) (*containerd.Client, error) { - address := context.GlobalString("address") - //timeout := context.GlobalDuration("connect-timeout") - - return containerd.New(address) -} - -// 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(clicontext *cli.Context) (contextpkg.Context, contextpkg.CancelFunc) { - var ( - ctx = contextpkg.Background() - timeout = clicontext.GlobalDuration("timeout") - namespace = clicontext.GlobalString("namespace") - cancel = func() {} - ) - - ctx = namespaces.WithNamespace(ctx, namespace) - - if timeout > 0 { - ctx, cancel = contextpkg.WithTimeout(ctx, timeout) - } else { - ctx, cancel = contextpkg.WithCancel(ctx) - } - - return ctx, cancel -} - -func resolveContentStore(context *cli.Context) (content.Store, error) { - client, err := getClient(context) - if err != nil { - return nil, err - } - return client.ContentStore(), nil -} - -// getResolver prepares the resolver from the environment and options. -func getResolver(ctx context.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 -} - -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 -} - -func getImageLayers(ctx context.Context, image images.Image, cs content.Store) ([]rootfs.Layer, error) { - p, err := content.ReadBlob(ctx, cs, image.Target.Digest) - if err != nil { - return nil, errors.Wrapf(err, "failed to read manifest blob") - } - - var manifest ocispec.Manifest - if err := json.Unmarshal(p, &manifest); err != nil { - return nil, errors.Wrap(err, "failed to unmarshal manifest") - } - - diffIDs, err := image.RootFS(ctx, cs) - if err != nil { - return nil, errors.Wrap(err, "failed to resolve rootfs") - } - - if len(diffIDs) != len(manifest.Layers) { - return nil, errors.Errorf("mismatched image rootfs and manifest layers") - } - - layers := make([]rootfs.Layer, len(diffIDs)) - for i := range diffIDs { - layers[i].Diff = ocispec.Descriptor{ - // TODO: derive media type from compressed type - MediaType: ocispec.MediaTypeImageLayer, - Digest: diffIDs[i], - } - layers[i].Blob = manifest.Layers[i] - } - - return layers, nil -} diff --git a/cmd/dist/main.go b/cmd/dist/main.go deleted file mode 100644 index f6fc59d65..000000000 --- a/cmd/dist/main.go +++ /dev/null @@ -1,79 +0,0 @@ -package main - -import ( - "fmt" - "os" - "time" - - "github.com/Sirupsen/logrus" - "github.com/containerd/containerd" - namespaces2 "github.com/containerd/containerd/namespaces" - "github.com/containerd/containerd/version" - "github.com/urfave/cli" -) - -var ( - timeout time.Duration -) - -func init() { - cli.VersionPrinter = func(c *cli.Context) { - fmt.Println(c.App.Name, version.Package, c.App.Version) - } - -} - -func main() { - app := cli.NewApp() - app.Name = "dist" - app.Version = version.Version - app.Usage = ` - ___ __ - ____/ (_)____/ /_ - / __ / / ___/ __/ - / /_/ / (__ ) /_ - \__,_/_/____/\__/ - -distribution tool -` - app.Flags = []cli.Flag{ - cli.BoolFlag{ - Name: "debug", - Usage: "enable debug output in logs", - }, - cli.DurationFlag{ - Name: "timeout", - Usage: "total timeout for dist commands", - }, - cli.StringFlag{ - Name: "address, a", - Usage: "address for containerd's GRPC server", - Value: containerd.DefaultAddress, - }, - cli.StringFlag{ - Name: "namespace, n", - Usage: "namespace to use with commands", - Value: namespaces2.Default, - EnvVar: namespaces2.NamespaceEnvVar, - }, - } - app.Commands = []cli.Command{ - pullCommand, - fetchCommand, - fetchObjectCommand, - applyCommand, - rootfsCommand, - pushCommand, - pushObjectCommand, - } - app.Before = func(context *cli.Context) error { - if context.GlobalBool("debug") { - logrus.SetLevel(logrus.DebugLevel) - } - return nil - } - if err := app.Run(os.Args); err != nil { - fmt.Fprintf(os.Stderr, "dist: %s\n", err) - os.Exit(1) - } -}