From 98b30f469055a06aed02be6c392e15fdbce534ef Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Sat, 25 Apr 2020 16:00:06 -0700 Subject: [PATCH] Add commands to mount/unmount image from ref Example: ```terminal $ mkdir /opt/busybox $ ctr image mount docker.io/library/busybox:latest /opt/busybox /opt/busybox $ ls -lh /opt/busybox total 40K drwxr-xr-x 2 root root 12K Apr 14 01:10 bin drwxr-xr-x 2 root root 4.0K Apr 14 01:10 dev drwxr-xr-x 3 root root 4.0K Apr 14 01:10 etc drwxr-xr-x 2 nobody nogroup 4.0K Apr 14 01:10 home drwx------ 2 root root 4.0K Apr 14 01:10 root drwxrwxrwt 2 root root 4.0K Apr 14 01:10 tmp drwxr-xr-x 3 root root 4.0K Apr 14 01:10 usr drwxr-xr-x 4 root root 4.0K Apr 14 01:10 var $ ctr image unmount /opt/busybox $ ls -lh /opt/busybox total 0 ``` Signed-off-by: Brian Goff --- cmd/ctr/commands/images/images.go | 2 + cmd/ctr/commands/images/mount.go | 143 +++++++++++++++++++++++++++++ cmd/ctr/commands/images/unmount.go | 73 +++++++++++++++ 3 files changed, 218 insertions(+) create mode 100644 cmd/ctr/commands/images/mount.go create mode 100644 cmd/ctr/commands/images/unmount.go diff --git a/cmd/ctr/commands/images/images.go b/cmd/ctr/commands/images/images.go index 44e5fca91..49cc2149b 100644 --- a/cmd/ctr/commands/images/images.go +++ b/cmd/ctr/commands/images/images.go @@ -43,6 +43,8 @@ var Command = cli.Command{ exportCommand, importCommand, listCommand, + mountCommand, + unmountCommand, pullCommand, pushCommand, removeCommand, diff --git a/cmd/ctr/commands/images/mount.go b/cmd/ctr/commands/images/mount.go new file mode 100644 index 000000000..b28a14c5d --- /dev/null +++ b/cmd/ctr/commands/images/mount.go @@ -0,0 +1,143 @@ +package images + +import ( + "fmt" + "time" + + "github.com/containerd/containerd" + "github.com/containerd/containerd/cmd/ctr/commands" + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/leases" + "github.com/containerd/containerd/mount" + "github.com/containerd/containerd/platforms" + "github.com/opencontainers/image-spec/identity" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +/* + 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. +*/ + +var mountCommand = cli.Command{ + Name: "mount", + Usage: "mount an image to a target path", + ArgsUsage: "[flags] ", + Description: `Mount an image rootfs to a specified path. + +When you are done, use the unmount command. +`, + Flags: append(append(commands.RegistryFlags, append(commands.SnapshotterFlags, commands.LabelFlag)...), + cli.BoolFlag{ + Name: "rw", + Usage: "Enable write support on the mount", + }, + cli.StringFlag{ + Name: "platform", + Usage: "Mount the image for the specified platform", + Value: platforms.DefaultString(), + }, + ), + Action: func(context *cli.Context) (retErr error) { + var ( + ref = context.Args().First() + target = context.Args().Get(1) + ) + if ref == "" { + return fmt.Errorf("please provide an image reference to mount") + } + if target == "" { + return fmt.Errorf("please provide a target path to mount to") + } + + client, ctx, cancel, err := commands.NewClient(context) + if err != nil { + return err + } + defer cancel() + + snapshotter := context.GlobalString("snapshotter") + if snapshotter == "" { + snapshotter = containerd.DefaultSnapshotter + } + + ctx, done, err := client.WithLease(ctx, + leases.WithID(target), + leases.WithExpiration(24*time.Hour), + leases.WithLabels(map[string]string{ + "containerd.io/gc.ref.snapshot." + snapshotter: target, + }), + ) + if err != nil && !errdefs.IsAlreadyExists(err) { + return err + } + + defer func() { + if retErr != nil && done != nil { + done(ctx) + } + }() + + ps := context.String("platform") + p, err := platforms.Parse(ps) + if err != nil { + return errors.Wrapf(err, "unable to parse platform %s", ps) + } + + img, err := client.ImageService().Get(ctx, ref) + if err != nil { + return err + } + + i := containerd.NewImageWithPlatform(client, img, platforms.Only(p)) + if err := i.Unpack(ctx, snapshotter); err != nil { + return errors.Wrap(err, "error unpacking image") + } + + diffIDs, err := i.RootFS(ctx) + if err != nil { + return err + } + chainID := identity.ChainID(diffIDs).String() + fmt.Println(chainID) + + s := client.SnapshotService(snapshotter) + + var mounts []mount.Mount + if context.Bool("rw") { + mounts, err = s.Prepare(ctx, target, chainID) + } else { + mounts, err = s.View(ctx, target, chainID) + } + if err != nil { + if errdefs.IsAlreadyExists(err) { + mounts, err = s.Mounts(ctx, target) + } + if err != nil { + return err + } + } + + if err := mount.All(mounts, target); err != nil { + if err := s.Remove(ctx, target); err != nil && !errdefs.IsNotFound(err) { + fmt.Fprintln(context.App.ErrWriter, "Error cleaning up snapshot after mount error:", err) + } + return err + } + + fmt.Fprintln(context.App.Writer, target) + return nil + }, +} diff --git a/cmd/ctr/commands/images/unmount.go b/cmd/ctr/commands/images/unmount.go new file mode 100644 index 000000000..f5c1ad7b2 --- /dev/null +++ b/cmd/ctr/commands/images/unmount.go @@ -0,0 +1,73 @@ +package images + +import ( + "fmt" + + "github.com/containerd/containerd/cmd/ctr/commands" + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/leases" + "github.com/containerd/containerd/mount" + "github.com/pkg/errors" + "github.com/urfave/cli" +) + +/* + 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. +*/ + +var unmountCommand = cli.Command{ + Name: "unmount", + Usage: "unmount the image from the target", + ArgsUsage: "[flags] ", + Description: "Unmount the image rootfs from the specified target.", + Flags: append(append(commands.RegistryFlags, append(commands.SnapshotterFlags, commands.LabelFlag)...), + cli.BoolFlag{ + Name: "rm", + Usage: "remove the snapshot after a successful unmount", + }, + ), + Action: func(context *cli.Context) error { + var ( + target = context.Args().First() + ) + if target == "" { + return fmt.Errorf("please provide a target path to mount to") + } + + client, ctx, cancel, err := commands.NewClient(context) + if err != nil { + return err + } + defer cancel() + + if err := mount.UnmountAll(target, 0); err != nil { + return err + } + + if context.Bool("rm") { + snapshotter := context.String("snapshotter") + s := client.SnapshotService(snapshotter) + if err := client.LeasesService().Delete(ctx, leases.Lease{ID: target}); err != nil && !errdefs.IsNotFound(err) { + return errors.Wrap(err, "error deleting lease") + } + if err := s.Remove(ctx, target); err != nil && !errdefs.IsNotFound(err) { + return errors.Wrap(err, "error removing snapshot") + } + } + + fmt.Fprintln(context.App.Writer, target) + return nil + }, +}