diff --git a/client_test.go b/client_test.go index db80a9c30..eb6d8539a 100644 --- a/client_test.go +++ b/client_test.go @@ -9,7 +9,6 @@ import ( golog "log" "os" "os/exec" - "runtime" "testing" "time" @@ -17,6 +16,7 @@ import ( "github.com/containerd/containerd/log" "github.com/containerd/containerd/namespaces" + "github.com/containerd/containerd/sys" "github.com/containerd/containerd/testutil" "github.com/sirupsen/logrus" ) @@ -62,7 +62,7 @@ func TestMain(m *testing.M) { defer cancel() if !noDaemon { - os.RemoveAll(defaultRoot) + sys.ForceRemoveAll(defaultRoot) err := ctrd.start("containerd", address, []string{ "--root", defaultRoot, @@ -99,17 +99,10 @@ func TestMain(m *testing.M) { }).Info("running tests against containerd") // pull a seed image - if runtime.GOOS != "windows" { // TODO: remove once pull is supported on windows - if _, err = client.Pull(ctx, testImage, WithPullUnpack); err != nil { - ctrd.Stop() - ctrd.Wait() - fmt.Fprintf(os.Stderr, "%s: %s\n", err, buf.String()) - os.Exit(1) - } - } - - if err := platformTestSetup(client); err != nil { - fmt.Fprintln(os.Stderr, "platform test setup failed", err) + if _, err = client.Pull(ctx, testImage, WithPullUnpack); err != nil { + ctrd.Stop() + ctrd.Wait() + fmt.Fprintf(os.Stderr, "%s: %s\n", err, buf.String()) os.Exit(1) } @@ -132,7 +125,7 @@ func TestMain(m *testing.M) { fmt.Fprintln(os.Stderr, "failed to wait for containerd", err) } } - if err := os.RemoveAll(defaultRoot); err != nil { + if err := sys.ForceRemoveAll(defaultRoot); err != nil { fmt.Fprintln(os.Stderr, "failed to remove test root dir", err) os.Exit(1) } @@ -169,11 +162,6 @@ func TestNewClient(t *testing.T) { // All the container's tests depends on this, we need it to run first. func TestImagePull(t *testing.T) { - if runtime.GOOS == "windows" { - // TODO: remove once Windows has a snapshotter - t.Skip("Windows does not have a snapshotter yet") - } - client, err := newClient(t, address) if err != nil { t.Fatal(err) diff --git a/client_unix_test.go b/client_unix_test.go index 27fe7fab1..62cce45ab 100644 --- a/client_unix_test.go +++ b/client_unix_test.go @@ -16,10 +16,6 @@ var ( testImage string ) -func platformTestSetup(client *Client) error { - return nil -} - func init() { switch runtime.GOARCH { case "386": diff --git a/client_windows_test.go b/client_windows_test.go index 115d71497..4cb3e580f 100644 --- a/client_windows_test.go +++ b/client_windows_test.go @@ -1,88 +1,16 @@ package containerd import ( - "encoding/json" - "fmt" "os" "path/filepath" - - "github.com/pkg/errors" ) const ( defaultAddress = `\\.\pipe\containerd-containerd-test` - testImage = "docker.io/library/go:nanoserver" + testImage = "docker.io/microsoft/nanoserver:latest" ) var ( - dockerLayerFolders []string - defaultRoot = filepath.Join(os.Getenv("programfiles"), "containerd", "root-test") defaultState = filepath.Join(os.Getenv("programfiles"), "containerd", "state-test") ) - -func platformTestSetup(client *Client) error { - var ( - roots []string - layerChains = make(map[string]string) - ) - // Since we can't pull images yet, we'll piggyback on the default - // docker's images - wfPath := `C:\ProgramData\docker\windowsfilter` - wf, err := os.Open(wfPath) - if err != nil { - return errors.Wrapf(err, "failed to access docker layers @ %s", wfPath) - } - defer wf.Close() - entries, err := wf.Readdirnames(0) - if err != nil { - return errors.Wrapf(err, "failed to read %s entries", wfPath) - } - - for _, fn := range entries { - layerChainPath := filepath.Join(wfPath, fn, "layerchain.json") - lfi, err := os.Stat(layerChainPath) - switch { - case err == nil && lfi.Mode().IsRegular(): - f, err := os.OpenFile(layerChainPath, os.O_RDONLY, 0660) - if err != nil { - fmt.Fprintln(os.Stderr, - errors.Wrapf(err, "failed to open %s", layerChainPath)) - continue - } - defer f.Close() - l := make([]string, 0) - if err := json.NewDecoder(f).Decode(&l); err != nil { - fmt.Fprintln(os.Stderr, - errors.Wrapf(err, "failed to decode %s", layerChainPath)) - continue - } - switch { - case len(l) == 1: - layerChains[l[0]] = filepath.Join(wfPath, fn) - case len(l) > 1: - fmt.Fprintf(os.Stderr, "Too many entries in %s: %d", layerChainPath, len(l)) - case len(l) == 0: - roots = append(roots, filepath.Join(wfPath, fn)) - } - case os.IsNotExist(err): - // keep on going - default: - return errors.Wrapf(err, "error trying to access %s", layerChainPath) - } - } - - // They'll be 2 roots, just take the first one - l := roots[0] - dockerLayerFolders = append(dockerLayerFolders, l) - for { - l = layerChains[l] - if l == "" { - break - } - - dockerLayerFolders = append([]string{l}, dockerLayerFolders...) - } - - return nil -} diff --git a/cmd/containerd/builtins_windows.go b/cmd/containerd/builtins_windows.go index 381489170..95c9fb2f7 100644 --- a/cmd/containerd/builtins_windows.go +++ b/cmd/containerd/builtins_windows.go @@ -1,6 +1,7 @@ package main import ( + _ "github.com/containerd/containerd/diff/windows" _ "github.com/containerd/containerd/snapshots/windows" _ "github.com/containerd/containerd/windows" ) diff --git a/cmd/ctr/commands/run/run_windows.go b/cmd/ctr/commands/run/run_windows.go index c33f2302b..82fca433a 100644 --- a/cmd/ctr/commands/run/run_windows.go +++ b/cmd/ctr/commands/run/run_windows.go @@ -7,32 +7,12 @@ import ( "github.com/containerd/containerd" "github.com/containerd/containerd/cmd/ctr/commands" "github.com/containerd/containerd/containers" - "github.com/containerd/containerd/errdefs" "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" ) -func init() { - Command.Flags = append(Command.Flags, cli.StringSliceFlag{ - Name: "layer", - Usage: "HCSSHIM Layers to be used", - }) -} - -func withLayers(context *cli.Context) oci.SpecOpts { - return func(ctx gocontext.Context, client oci.Client, c *containers.Container, s *specs.Spec) error { - l := context.StringSlice("layer") - if l == nil { - return errors.Wrap(errdefs.ErrInvalidArgument, "base layers must be specified with `--layer`") - } - s.Windows.LayerFolders = l - return nil - } -} - func withTTY(terminal bool) oci.SpecOpts { if !terminal { return func(ctx gocontext.Context, client oci.Client, c *containers.Container, s *specs.Spec) error { @@ -51,36 +31,39 @@ func withTTY(terminal bool) oci.SpecOpts { 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:] - tty = context.Bool("tty") - labelStrings = context.StringSlice("label") + ref = context.Args().First() + id = context.Args().Get(1) + args = context.Args()[2:] ) - labels := commands.LabelArgs(labelStrings) - - // TODO(mlaventure): get base image once we have a snapshotter - - opts := []oci.SpecOpts{ - // TODO(mlaventure): use oci.WithImageConfig once we have a snapshotter - withLayers(context), - oci.WithEnv(context.StringSlice("env")), - withMounts(context), - withTTY(tty), + image, err := client.GetImage(ctx, ref) + if err != nil { + return nil, err } + + var ( + opts []oci.SpecOpts + cOpts []containerd.NewContainerOpts + ) + opts = append(opts, oci.WithImageConfig(image)) + opts = append(opts, oci.WithEnv(context.StringSlice("env"))) + opts = append(opts, withMounts(context)) if len(args) > 0 { opts = append(opts, oci.WithProcessArgs(args...)) } if cwd := context.String("cwd"); cwd != "" { opts = append(opts, oci.WithProcessCwd(cwd)) } - return client.NewContainer(ctx, id, - containerd.WithNewSpec(opts...), - containerd.WithContainerLabels(labels), - containerd.WithRuntime(context.String("runtime"), nil), - // TODO(mlaventure): containerd.WithImage(image), - ) + opts = append(opts, withTTY(context.Bool("tty"))) + + 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([]containerd.NewContainerOpts{containerd.WithNewSpec(opts...)}, cOpts...) + return client.NewContainer(ctx, id, cOpts...) } func getNewTaskOpts(_ *cli.Context) []containerd.NewTaskOpts { diff --git a/container_linux_test.go b/container_linux_test.go index 82e3a9a1a..6dc6b6484 100644 --- a/container_linux_test.go +++ b/container_linux_test.go @@ -187,7 +187,7 @@ func TestDaemonRestart(t *testing.T) { t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "30")), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), withProcessArgs("sleep", "30")), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -266,7 +266,7 @@ func TestContainerAttach(t *testing.T) { t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withCat()), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), withCat()), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -427,8 +427,8 @@ func TestContainerUsername(t *testing.T) { // squid user in the alpine image has a uid of 31 container, err := client.NewContainer(ctx, id, - withNewSnapshot(id, image), - WithNewSpec(withImageConfig(image), oci.WithUsername("squid"), oci.WithProcessArgs("id", "-u")), + WithNewSnapshot(id, image), + WithNewSpec(oci.WithImageConfig(image), oci.WithUsername("squid"), oci.WithProcessArgs("id", "-u")), ) if err != nil { t.Fatal(err) @@ -487,7 +487,7 @@ func TestContainerAttachProcess(t *testing.T) { t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "100")), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), withProcessArgs("sleep", "100")), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -620,8 +620,8 @@ func TestContainerUserID(t *testing.T) { // adm user in the alpine image has a uid of 3 and gid of 4. container, err := client.NewContainer(ctx, id, - withNewSnapshot(id, image), - WithNewSpec(withImageConfig(image), oci.WithUserID(3), oci.WithProcessArgs("sh", "-c", "echo $(id -u):$(id -g)")), + WithNewSnapshot(id, image), + WithNewSpec(oci.WithImageConfig(image), oci.WithUserID(3), oci.WithProcessArgs("sh", "-c", "echo $(id -u):$(id -g)")), ) if err != nil { t.Fatal(err) @@ -674,8 +674,8 @@ func TestContainerKillAll(t *testing.T) { } container, err := client.NewContainer(ctx, id, - withNewSnapshot(id, image), - WithNewSpec(withImageConfig(image), + WithNewSnapshot(id, image), + WithNewSpec(oci.WithImageConfig(image), withProcessArgs("sh", "-c", "top"), oci.WithHostNamespace(specs.PIDNamespace), ), @@ -730,7 +730,7 @@ func TestShimSigkilled(t *testing.T) { if err != nil { t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image)), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image)), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -793,7 +793,7 @@ func TestDaemonRestartWithRunningShim(t *testing.T) { if err != nil { t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), oci.WithProcessArgs("sleep", "100")), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), oci.WithProcessArgs("sleep", "100")), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -877,8 +877,8 @@ func TestContainerRuntimeOptions(t *testing.T) { container, err := client.NewContainer( ctx, id, - WithNewSpec(withImageConfig(image), withExitStatus(7)), - withNewSnapshot(id, image), + WithNewSpec(oci.WithImageConfig(image), withExitStatus(7)), + WithNewSnapshot(id, image), WithRuntime("io.containerd.runtime.v1.linux", &runctypes.RuncOptions{Runtime: "no-runc"}), ) if err != nil { @@ -917,8 +917,8 @@ func TestContainerKillInitPidHost(t *testing.T) { } container, err := client.NewContainer(ctx, id, - withNewSnapshot(id, image), - WithNewSpec(withImageConfig(image), + WithNewSnapshot(id, image), + WithNewSpec(oci.WithImageConfig(image), withProcessArgs("sh", "-c", "sleep 42; echo hi"), oci.WithHostNamespace(specs.PIDNamespace), ), @@ -1007,7 +1007,7 @@ func testUserNamespaces(t *testing.T, readonlyRootFS bool) { t.Fatal(err) } - opts := []NewContainerOpts{WithNewSpec(withImageConfig(image), + opts := []NewContainerOpts{WithNewSpec(oci.WithImageConfig(image), withExitStatus(7), oci.WithUserNamespace(0, 1000, 10000), )} @@ -1081,13 +1081,11 @@ func TestTaskResize(t *testing.T) { ) defer cancel() - if runtime.GOOS != "windows" { - image, err = client.GetImage(ctx, testImage) - if err != nil { - t.Fatal(err) - } + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withExitStatus(7)), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), withExitStatus(7)), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } diff --git a/container_test.go b/container_test.go index d3ed29ee7..257bcd9fb 100644 --- a/container_test.go +++ b/container_test.go @@ -97,13 +97,11 @@ func TestContainerStart(t *testing.T) { ) defer cancel() - if runtime.GOOS != "windows" { - image, err = client.GetImage(ctx, testImage) - if err != nil { - t.Fatal(err) - } + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withExitStatus(7)), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), withExitStatus(7)), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -163,13 +161,11 @@ func TestContainerOutput(t *testing.T) { ) defer cancel() - if runtime.GOOS != "windows" { - image, err = client.GetImage(ctx, testImage) - if err != nil { - t.Fatal(err) - } + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("echo", expected)), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), withProcessArgs("echo", expected)), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -233,14 +229,12 @@ func TestContainerExec(t *testing.T) { ) defer cancel() - if runtime.GOOS != "windows" { - image, err = client.GetImage(ctx, testImage) - if err != nil { - t.Fatal(err) - } + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "100")), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), withProcessArgs("sleep", "100")), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -320,14 +314,12 @@ func TestContainerLargeExecArgs(t *testing.T) { ) defer cancel() - if runtime.GOOS != "windows" { - image, err = client.GetImage(ctx, testImage) - if err != nil { - t.Fatal(err) - } + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "100")), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), withProcessArgs("sleep", "100")), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -398,14 +390,12 @@ func TestContainerPids(t *testing.T) { ) defer cancel() - if runtime.GOOS != "windows" { - image, err = client.GetImage(ctx, testImage) - if err != nil { - t.Fatal(err) - } + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "100")), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), withProcessArgs("sleep", "100")), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -477,14 +467,12 @@ func TestContainerCloseIO(t *testing.T) { ) defer cancel() - if runtime.GOOS != "windows" { - image, err = client.GetImage(ctx, testImage) - if err != nil { - t.Fatal(err) - } + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withCat()), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), withCat()), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -535,14 +523,12 @@ func TestDeleteRunningContainer(t *testing.T) { ) defer cancel() - if runtime.GOOS != "windows" { - image, err = client.GetImage(ctx, testImage) - if err != nil { - t.Fatal(err) - } + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "100")), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), withProcessArgs("sleep", "100")), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -592,14 +578,12 @@ func TestContainerKill(t *testing.T) { ) defer cancel() - if runtime.GOOS != "windows" { - image, err = client.GetImage(ctx, testImage) - if err != nil { - t.Fatal(err) - } + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "10")), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), withProcessArgs("sleep", "10")), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -649,16 +633,14 @@ func TestContainerNoBinaryExists(t *testing.T) { ) defer cancel() - if runtime.GOOS != "windows" { - image, err = client.GetImage(ctx, testImage) - if err != nil { - t.Fatal(err) - } + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) } container, err := client.NewContainer(ctx, id, - WithNewSpec(withImageConfig(image), oci.WithProcessArgs("nothing")), - withNewSnapshot(id, image)) + WithNewSpec(oci.WithImageConfig(image), oci.WithProcessArgs("nothing")), + WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -698,14 +680,12 @@ func TestContainerExecNoBinaryExists(t *testing.T) { ) defer cancel() - if runtime.GOOS != "windows" { - image, err = client.GetImage(ctx, testImage) - if err != nil { - t.Fatal(err) - } + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "100")), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), withProcessArgs("sleep", "100")), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -765,14 +745,12 @@ func TestWaitStoppedTask(t *testing.T) { ) defer cancel() - if runtime.GOOS != "windows" { - image, err = client.GetImage(ctx, testImage) - if err != nil { - t.Fatal(err) - } + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withExitStatus(7)), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), withExitStatus(7)), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -830,14 +808,12 @@ func TestWaitStoppedProcess(t *testing.T) { ) defer cancel() - if runtime.GOOS != "windows" { - image, err = client.GetImage(ctx, testImage) - if err != nil { - t.Fatal(err) - } + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "100")), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), withProcessArgs("sleep", "100")), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -920,13 +896,11 @@ func TestTaskForceDelete(t *testing.T) { ) defer cancel() - if runtime.GOOS != "windows" { - image, err = client.GetImage(ctx, testImage) - if err != nil { - t.Fatal(err) - } + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "30")), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), withProcessArgs("sleep", "30")), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -963,13 +937,11 @@ func TestProcessForceDelete(t *testing.T) { ) defer cancel() - if runtime.GOOS != "windows" { - image, err = client.GetImage(ctx, testImage) - if err != nil { - t.Fatal(err) - } + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "30")), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), withProcessArgs("sleep", "30")), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -1034,18 +1006,16 @@ func TestContainerHostname(t *testing.T) { ) defer cancel() - if runtime.GOOS != "windows" { - image, err = client.GetImage(ctx, testImage) - if err != nil { - t.Fatal(err) - } + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), withProcessArgs("hostname"), oci.WithHostname(expected), ), - withNewSnapshot(id, image)) + WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -1105,14 +1075,12 @@ func TestContainerExitedAtSet(t *testing.T) { ) defer cancel() - if runtime.GOOS != "windows" { - image, err = client.GetImage(ctx, testImage) - if err != nil { - t.Fatal(err) - } + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withTrue()), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), withTrue()), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -1167,14 +1135,12 @@ func TestDeleteContainerExecCreated(t *testing.T) { ) defer cancel() - if runtime.GOOS != "windows" { - image, err = client.GetImage(ctx, testImage) - if err != nil { - t.Fatal(err) - } + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) } - container, err := client.NewContainer(ctx, id, WithNewSpec(withImageConfig(image), withProcessArgs("sleep", "100")), withNewSnapshot(id, image)) + container, err := client.NewContainer(ctx, id, WithNewSpec(oci.WithImageConfig(image), withProcessArgs("sleep", "100")), WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -1239,15 +1205,13 @@ func TestContainerMetrics(t *testing.T) { ) defer cancel() - if runtime.GOOS != "windows" { - image, err = client.GetImage(ctx, testImage) - if err != nil { - t.Fatal(err) - } + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) } container, err := client.NewContainer(ctx, id, - WithNewSpec(withImageConfig(image), oci.WithProcessArgs("sleep", "30")), - withNewSnapshot(id, image)) + WithNewSpec(oci.WithImageConfig(image), oci.WithProcessArgs("sleep", "30")), + WithNewSnapshot(id, image)) if err != nil { t.Fatal(err) } @@ -1297,15 +1261,13 @@ func TestDeletedContainerMetrics(t *testing.T) { ) defer cancel() - if runtime.GOOS != "windows" { - image, err = client.GetImage(ctx, testImage) - if err != nil { - t.Fatal(err) - } + image, err = client.GetImage(ctx, testImage) + if err != nil { + t.Fatal(err) } container, err := client.NewContainer(ctx, id, - WithNewSpec(withImageConfig(image), withExitStatus(0)), - withNewSnapshot(id, image), + WithNewSpec(oci.WithImageConfig(image), withExitStatus(0)), + WithNewSnapshot(id, image), ) if err != nil { t.Fatal(err) diff --git a/diff/walking/differ.go b/diff/walking/differ.go index eb6c2df70..f1dbf43f5 100644 --- a/diff/walking/differ.go +++ b/diff/walking/differ.go @@ -6,7 +6,6 @@ import ( "fmt" "io" "io/ioutil" - "strings" "time" "github.com/containerd/containerd/archive" @@ -75,18 +74,10 @@ func (s *walkingDiff) Apply(ctx context.Context, desc ocispec.Descriptor, mounts }).Debugf("diff applied") } }() - var isCompressed bool - switch desc.MediaType { - case ocispec.MediaTypeImageLayer, images.MediaTypeDockerSchema2Layer: - case ocispec.MediaTypeImageLayerGzip, images.MediaTypeDockerSchema2LayerGzip: - isCompressed = true - default: - // Still apply all generic media types *.tar[.+]gzip and *.tar - if strings.HasSuffix(desc.MediaType, ".tar.gzip") || strings.HasSuffix(desc.MediaType, ".tar+gzip") { - isCompressed = true - } else if !strings.HasSuffix(desc.MediaType, ".tar") { - return emptyDesc, errors.Wrapf(errdefs.ErrNotImplemented, "unsupported diff media type: %v", desc.MediaType) - } + + isCompressed, err := images.IsCompressedDiff(ctx, desc.MediaType) + if err != nil { + return emptyDesc, errors.Wrapf(errdefs.ErrNotImplemented, "unsupported diff media type: %v", desc.MediaType) } var ocidesc ocispec.Descriptor diff --git a/diff/windows/windows.go b/diff/windows/windows.go new file mode 100644 index 000000000..0bb79e15e --- /dev/null +++ b/diff/windows/windows.go @@ -0,0 +1,160 @@ +// +build windows + +package windows + +import ( + "io" + "io/ioutil" + "time" + + winio "github.com/Microsoft/go-winio" + "github.com/containerd/containerd/archive" + "github.com/containerd/containerd/archive/compression" + "github.com/containerd/containerd/content" + "github.com/containerd/containerd/diff" + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/images" + "github.com/containerd/containerd/log" + "github.com/containerd/containerd/metadata" + "github.com/containerd/containerd/mount" + "github.com/containerd/containerd/platforms" + "github.com/containerd/containerd/plugin" + digest "github.com/opencontainers/go-digest" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "golang.org/x/net/context" +) + +func init() { + plugin.Register(&plugin.Registration{ + Type: plugin.DiffPlugin, + ID: "windows", + Requires: []plugin.Type{ + plugin.MetadataPlugin, + }, + InitFn: func(ic *plugin.InitContext) (interface{}, error) { + md, err := ic.Get(plugin.MetadataPlugin) + if err != nil { + return nil, err + } + + ic.Meta.Platforms = append(ic.Meta.Platforms, platforms.DefaultSpec()) + return NewWindowsDiff(md.(*metadata.DB).ContentStore()) + }, + }) +} + +type windowsDiff struct { + store content.Store +} + +var emptyDesc = ocispec.Descriptor{} + +// NewWindowsDiff is the Windows container layer implementation of diff.Differ. +func NewWindowsDiff(store content.Store) (diff.Differ, error) { + return &windowsDiff{ + store: store, + }, nil +} + +// Apply applies the content associated with the provided digests onto the +// provided mounts. Archive content will be extracted and decompressed if +// necessary. +func (s *windowsDiff) Apply(ctx context.Context, desc ocispec.Descriptor, mounts []mount.Mount) (d ocispec.Descriptor, err error) { + t1 := time.Now() + defer func() { + if err == nil { + log.G(ctx).WithFields(logrus.Fields{ + "d": time.Now().Sub(t1), + "dgst": desc.Digest, + "size": desc.Size, + "media": desc.MediaType, + }).Debugf("diff applied") + } + }() + + isCompressed, err := images.IsCompressedDiff(ctx, desc.MediaType) + if err != nil { + return emptyDesc, errors.Wrapf(errdefs.ErrNotImplemented, "unsupported diff media type: %v", desc.MediaType) + } + + ra, err := s.store.ReaderAt(ctx, desc.Digest) + if err != nil { + return emptyDesc, errors.Wrap(err, "failed to get reader from content store") + } + defer ra.Close() + + r := content.NewReader(ra) + if isCompressed { + ds, err := compression.DecompressStream(r) + if err != nil { + return emptyDesc, err + } + defer ds.Close() + r = ds + } + + digester := digest.Canonical.Digester() + rc := &readCounter{ + r: io.TeeReader(r, digester.Hash()), + } + + layer, parentLayerPaths, err := mountsToLayerAndParents(mounts) + if err != nil { + return emptyDesc, err + } + + // TODO darrenstahlmsft: When this is done isolated, we should disable these. + // it currently cannot be disabled, unless we add ref counting. Since this is + // temporary, leaving it enabled is OK for now. + if err := winio.EnableProcessPrivileges([]string{winio.SeBackupPrivilege, winio.SeRestorePrivilege}); err != nil { + return emptyDesc, err + } + + if _, err := archive.Apply(ctx, layer, rc, archive.WithParentLayers(parentLayerPaths), archive.AsWindowsContainerLayer()); err != nil { + return emptyDesc, err + } + + // Read any trailing data + if _, err := io.Copy(ioutil.Discard, rc); err != nil { + return emptyDesc, err + } + + return ocispec.Descriptor{ + MediaType: ocispec.MediaTypeImageLayer, + Size: rc.c, + Digest: digester.Digest(), + }, nil +} + +// DiffMounts creates a diff between the given mounts and uploads the result +// to the content store. +func (s *windowsDiff) DiffMounts(ctx context.Context, lower, upper []mount.Mount, opts ...diff.Opt) (d ocispec.Descriptor, err error) { + return emptyDesc, errdefs.ErrNotImplemented +} + +type readCounter struct { + r io.Reader + c int64 +} + +func (rc *readCounter) Read(p []byte) (n int, err error) { + n, err = rc.r.Read(p) + rc.c += int64(n) + return +} + +func mountsToLayerAndParents(mounts []mount.Mount) (string, []string, error) { + if len(mounts) != 1 { + return "", nil, errors.Wrap(errdefs.ErrInvalidArgument, "number of mounts should always be 1 for Windows layers") + } + layer := mounts[0].Source + + parentLayerPaths, err := mounts[0].GetParentPaths() + if err != nil { + return "", nil, err + } + + return layer, parentLayerPaths, nil +} diff --git a/helpers_unix_test.go b/helpers_unix_test.go index 51c034e10..8b028f9a1 100644 --- a/helpers_unix_test.go +++ b/helpers_unix_test.go @@ -39,8 +39,3 @@ func withExecExitStatus(s *specs.Process, es int) { func withExecArgs(s *specs.Process, args ...string) { s.Args = args } - -var ( - withNewSnapshot = WithNewSnapshot - withImageConfig = oci.WithImageConfig -) diff --git a/helpers_windows_test.go b/helpers_windows_test.go index a3b4768f6..83ecb7602 100644 --- a/helpers_windows_test.go +++ b/helpers_windows_test.go @@ -39,17 +39,3 @@ func withExecExitStatus(s *specs.Process, es int) { func withExecArgs(s *specs.Process, args ...string) { s.Args = append([]string{"powershell", "-noprofile"}, args...) } - -func withImageConfig(i Image) oci.SpecOpts { - return func(_ context.Context, _ oci.Client, _ *containers.Container, s *specs.Spec) error { - s.Windows.LayerFolders = dockerLayerFolders - return nil - } -} - -func withNewSnapshot(id string, i Image) NewContainerOpts { - // TODO: when windows has a snapshotter remove the withNewSnapshot helper - return func(ctx context.Context, client *Client, c *containers.Container) error { - return nil - } -} diff --git a/images/image.go b/images/image.go index 3f58e0620..5fea0dcfc 100644 --- a/images/image.go +++ b/images/image.go @@ -3,6 +3,7 @@ package images import ( "context" "encoding/json" + "strings" "time" "github.com/containerd/containerd/content" @@ -359,3 +360,22 @@ func RootFS(ctx context.Context, provider content.Provider, configDesc ocispec.D } return config.RootFS.DiffIDs, nil } + +// IsCompressedDiff returns true if mediaType is a known compressed diff media type. +// It returns false if the media type is a diff, but not compressed. If the media type +// is not a known diff type, it returns errdefs.ErrNotImplemented +func IsCompressedDiff(ctx context.Context, mediaType string) (bool, error) { + switch mediaType { + case ocispec.MediaTypeImageLayer, MediaTypeDockerSchema2Layer: + case ocispec.MediaTypeImageLayerGzip, MediaTypeDockerSchema2LayerGzip: + return true, nil + default: + // Still apply all generic media types *.tar[.+]gzip and *.tar + if strings.HasSuffix(mediaType, ".tar.gzip") || strings.HasSuffix(mediaType, ".tar+gzip") { + return true, nil + } else if !strings.HasSuffix(mediaType, ".tar") { + return false, errdefs.ErrNotImplemented + } + } + return false, nil +} diff --git a/mount/mount_windows.go b/mount/mount_windows.go index 8ad7eab12..6d37fda2a 100644 --- a/mount/mount_windows.go +++ b/mount/mount_windows.go @@ -1,6 +1,13 @@ package mount -import "github.com/pkg/errors" +import ( + "encoding/json" + "path/filepath" + "strings" + + "github.com/Microsoft/hcsshim" + "github.com/pkg/errors" +) var ( // ErrNotImplementOnWindows is returned when an action is not implemented for windows @@ -9,15 +16,70 @@ var ( // Mount to the provided target func (m *Mount) Mount(target string) error { - return ErrNotImplementOnWindows + home, layerID := filepath.Split(m.Source) + + parentLayerPaths, err := m.GetParentPaths() + if err != nil { + return err + } + + var di = hcsshim.DriverInfo{ + HomeDir: home, + } + + if err = hcsshim.ActivateLayer(di, layerID); err != nil { + return errors.Wrapf(err, "failed to activate layer %s", m.Source) + } + defer func() { + if err != nil { + hcsshim.DeactivateLayer(di, layerID) + } + }() + + if err = hcsshim.PrepareLayer(di, layerID, parentLayerPaths); err != nil { + return errors.Wrapf(err, "failed to prepare layer %s", m.Source) + } + return nil +} + +// ParentLayerPathsFlag is the options flag used to represent the JSON encoded +// list of parent layers required to use the layer +const ParentLayerPathsFlag = "parentLayerPaths=" + +// GetParentPaths of the mount +func (m *Mount) GetParentPaths() ([]string, error) { + var parentLayerPaths []string + for _, option := range m.Options { + if strings.HasPrefix(option, ParentLayerPathsFlag) { + err := json.Unmarshal([]byte(option[len(ParentLayerPathsFlag):]), &parentLayerPaths) + if err != nil { + return nil, errors.Wrap(err, "failed to unmarshal parent layer paths from mount") + } + } + } + return parentLayerPaths, nil } // Unmount the mount at the provided path func Unmount(mount string, flags int) error { - return ErrNotImplementOnWindows + var ( + home, layerID = filepath.Split(mount) + di = hcsshim.DriverInfo{ + HomeDir: home, + } + ) + + if err := hcsshim.UnprepareLayer(di, layerID); err != nil { + return errors.Wrapf(err, "failed to unprepare layer %s", mount) + } + if err := hcsshim.DeactivateLayer(di, layerID); err != nil { + return errors.Wrapf(err, "failed to deactivate layer %s", mount) + } + + return nil } -// UnmountAll mounts at the provided path +// UnmountAll unmounts from the provided path func UnmountAll(mount string, flags int) error { - return ErrNotImplementOnWindows + return Unmount(mount, flags) } diff --git a/services/diff/service.go b/services/diff/service.go index f847693a4..4e1c9fcc6 100644 --- a/services/diff/service.go +++ b/services/diff/service.go @@ -30,9 +30,7 @@ func init() { Requires: []plugin.Type{ plugin.DiffPlugin, }, - Config: &config{ - Order: []string{"walking"}, - }, + Config: defaultDifferConfig, InitFn: func(ic *plugin.InitContext) (interface{}, error) { differs, err := ic.GetByType(plugin.DiffPlugin) if err != nil { diff --git a/services/diff/service_unix.go b/services/diff/service_unix.go new file mode 100644 index 000000000..39d88bbbb --- /dev/null +++ b/services/diff/service_unix.go @@ -0,0 +1,7 @@ +// +build !windows + +package diff + +var defaultDifferConfig = &config{ + Order: []string{"walking"}, +} diff --git a/services/diff/service_windows.go b/services/diff/service_windows.go new file mode 100644 index 000000000..afe193287 --- /dev/null +++ b/services/diff/service_windows.go @@ -0,0 +1,7 @@ +// +build windows + +package diff + +var defaultDifferConfig = &config{ + Order: []string{"windows"}, +} diff --git a/snapshots/windows/windows.go b/snapshots/windows/windows.go index 603428e17..58469dd4f 100644 --- a/snapshots/windows/windows.go +++ b/snapshots/windows/windows.go @@ -4,16 +4,23 @@ package windows import ( "context" + "encoding/json" + "os" + "path/filepath" + "strings" + "syscall" + "unsafe" + "github.com/Microsoft/hcsshim" + "github.com/containerd/containerd/errdefs" + "github.com/containerd/containerd/fs" + "github.com/containerd/containerd/log" "github.com/containerd/containerd/mount" "github.com/containerd/containerd/plugin" "github.com/containerd/containerd/snapshots" + "github.com/containerd/containerd/snapshots/storage" "github.com/pkg/errors" -) - -var ( - // ErrNotImplemented is returned when an action is not implemented - ErrNotImplemented = errors.New("not implemented") + "golang.org/x/sys/windows" ) func init() { @@ -28,12 +35,38 @@ func init() { type snapshotter struct { root string + info hcsshim.DriverInfo + ms *storage.MetaStore } // NewSnapshotter returns a new windows snapshotter func NewSnapshotter(root string) (snapshots.Snapshotter, error) { + fsType, err := getFileSystemType(root) + if err != nil { + return nil, err + } + if strings.ToLower(fsType) != "ntfs" { + return nil, errors.Wrapf(errdefs.ErrInvalidArgument, "%s is not on an NTFS volume - only NTFS volumes are supported", root) + } + + if err := os.MkdirAll(root, 0700); err != nil { + return nil, err + } + ms, err := storage.NewMetaStore(filepath.Join(root, "metadata.db")) + if err != nil { + return nil, err + } + + if err := os.Mkdir(filepath.Join(root, "snapshots"), 0700); err != nil && !os.IsExist(err) { + return nil, err + } + return &snapshotter{ + info: hcsshim.DriverInfo{ + HomeDir: filepath.Join(root, "snapshots"), + }, root: root, + ms: ms, }, nil } @@ -42,50 +75,270 @@ func NewSnapshotter(root string) (snapshots.Snapshotter, error) { // // Should be used for parent resolution, existence checks and to discern // the kind of snapshot. -func (o *snapshotter) Stat(ctx context.Context, key string) (snapshots.Info, error) { - panic("not implemented") +func (s *snapshotter) Stat(ctx context.Context, key string) (snapshots.Info, error) { + ctx, t, err := s.ms.TransactionContext(ctx, false) + if err != nil { + return snapshots.Info{}, err + } + defer t.Rollback() + + _, info, _, err := storage.GetInfo(ctx, key) + return info, err } -func (o *snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (snapshots.Info, error) { - panic("not implemented") +func (s *snapshotter) Update(ctx context.Context, info snapshots.Info, fieldpaths ...string) (snapshots.Info, error) { + ctx, t, err := s.ms.TransactionContext(ctx, true) + if err != nil { + return snapshots.Info{}, err + } + defer t.Rollback() + + info, err = storage.UpdateInfo(ctx, info, fieldpaths...) + if err != nil { + return snapshots.Info{}, err + } + + if err := t.Commit(); err != nil { + return snapshots.Info{}, err + } + + return info, nil } -func (o *snapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) { - panic("not implemented") +func (s *snapshotter) Usage(ctx context.Context, key string) (snapshots.Usage, error) { + ctx, t, err := s.ms.TransactionContext(ctx, false) + if err != nil { + return snapshots.Usage{}, err + } + defer t.Rollback() + + _, info, usage, err := storage.GetInfo(ctx, key) + if err != nil { + return snapshots.Usage{}, err + } + + if info.Kind == snapshots.KindActive { + du := fs.Usage{ + Size: 0, + } + usage = snapshots.Usage(du) + } + + return usage, nil } -func (o *snapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) { - panic("not implemented") +func (s *snapshotter) Prepare(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) { + return s.createSnapshot(ctx, snapshots.KindActive, key, parent, opts) } -func (o *snapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) { - panic("not implemented") +func (s *snapshotter) View(ctx context.Context, key, parent string, opts ...snapshots.Opt) ([]mount.Mount, error) { + return s.createSnapshot(ctx, snapshots.KindView, key, parent, opts) } // Mounts returns the mounts for the transaction identified by key. Can be // called on an read-write or readonly transaction. // // This can be used to recover mounts after calling View or Prepare. -func (o *snapshotter) Mounts(ctx context.Context, key string) ([]mount.Mount, error) { - panic("not implemented") +func (s *snapshotter) Mounts(ctx context.Context, key string) ([]mount.Mount, error) { + ctx, t, err := s.ms.TransactionContext(ctx, false) + if err != nil { + return nil, err + } + defer t.Rollback() + + snapshot, err := storage.GetSnapshot(ctx, key) + if err != nil { + return nil, errors.Wrap(err, "failed to get snapshot mount") + } + return s.mounts(snapshot), nil } -func (o *snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error { - panic("not implemented") +func (s *snapshotter) Commit(ctx context.Context, name, key string, opts ...snapshots.Opt) error { + ctx, t, err := s.ms.TransactionContext(ctx, true) + if err != nil { + return err + } + defer t.Rollback() + + usage := fs.Usage{ + Size: 0, + } + + if _, err = storage.CommitActive(ctx, key, name, snapshots.Usage(usage), opts...); err != nil { + return errors.Wrap(err, "failed to commit snapshot") + } + + if err := t.Commit(); err != nil { + return err + } + return nil } // Remove abandons the transaction identified by key. All resources // associated with the key will be removed. -func (o *snapshotter) Remove(ctx context.Context, key string) error { - panic("not implemented") +func (s *snapshotter) Remove(ctx context.Context, key string) error { + ctx, t, err := s.ms.TransactionContext(ctx, true) + if err != nil { + return err + } + defer t.Rollback() + + id, _, err := storage.Remove(ctx, key) + if err != nil { + return errors.Wrap(err, "failed to remove") + } + + path := s.getSnapshotDir(id) + renamedID := "rm-" + id + renamed := filepath.Join(s.root, "snapshots", "rm-"+id) + if err := os.Rename(path, renamed); err != nil && !os.IsNotExist(err) { + return err + } + + if err := t.Commit(); 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") + } + return errors.Wrap(err, "failed to commit") + } + + if err := hcsshim.DestroyLayer(s.info, renamedID); 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") + } + + return nil } // Walk the committed snapshots. -func (o *snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshots.Info) error) error { - panic("not implemented") +func (s *snapshotter) Walk(ctx context.Context, fn func(context.Context, snapshots.Info) error) error { + ctx, t, err := s.ms.TransactionContext(ctx, false) + if err != nil { + return err + } + defer t.Rollback() + + return storage.WalkInfo(ctx, fn) } // Close closes the snapshotter -func (o *snapshotter) Close() error { - panic("not implemented") +func (s *snapshotter) Close() error { + return s.ms.Close() +} + +func (s *snapshotter) mounts(sn storage.Snapshot) []mount.Mount { + var ( + roFlag string + source string + parentLayerPaths []string + ) + + if sn.Kind == snapshots.KindView { + roFlag = "ro" + } else { + roFlag = "rw" + } + + if len(sn.ParentIDs) == 0 || sn.Kind == snapshots.KindActive { + source = s.getSnapshotDir(sn.ID) + parentLayerPaths = s.parentIDsToParentPaths(sn.ParentIDs) + } else { + source = s.getSnapshotDir(sn.ParentIDs[0]) + parentLayerPaths = s.parentIDsToParentPaths(sn.ParentIDs[1:]) + } + + // error is not checked here, as a string array will never fail to Marshal + parentLayersJSON, _ := json.Marshal(parentLayerPaths) + parentLayersOption := mount.ParentLayerPathsFlag + string(parentLayersJSON) + + var mounts []mount.Mount + mounts = append(mounts, mount.Mount{ + Source: source, + Type: "windows-layer", + Options: []string{ + roFlag, + parentLayersOption, + }, + }) + + return mounts +} + +func (s *snapshotter) getSnapshotDir(id string) string { + return filepath.Join(s.root, "snapshots", id) +} + +func (s *snapshotter) createSnapshot(ctx context.Context, kind snapshots.Kind, key, parent string, opts []snapshots.Opt) ([]mount.Mount, error) { + ctx, t, err := s.ms.TransactionContext(ctx, true) + if err != nil { + return nil, err + } + defer t.Rollback() + + newSnapshot, err := storage.CreateSnapshot(ctx, kind, key, parent, opts...) + if err != nil { + return nil, errors.Wrap(err, "failed to create snapshot") + } + + if kind == snapshots.KindActive { + parentLayerPaths := s.parentIDsToParentPaths(newSnapshot.ParentIDs) + + var parentPath string + if len(parentLayerPaths) != 0 { + parentPath = parentLayerPaths[0] + } + + if err := hcsshim.CreateSandboxLayer(s.info, newSnapshot.ID, parentPath, parentLayerPaths); err != nil { + return nil, errors.Wrap(err, "failed to create sandbox layer") + } + + // TODO(darrenstahlmsft): Allow changing sandbox size + } + + if err := t.Commit(); err != nil { + return nil, errors.Wrap(err, "commit failed") + } + + return s.mounts(newSnapshot), nil +} + +func (s *snapshotter) parentIDsToParentPaths(parentIDs []string) []string { + var parentLayerPaths []string + for _, ID := range parentIDs { + parentLayerPaths = append(parentLayerPaths, s.getSnapshotDir(ID)) + } + return parentLayerPaths +} + +// getFileSystemType obtains the type of a file system through GetVolumeInformation +// https://msdn.microsoft.com/en-us/library/windows/desktop/aa364993(v=vs.85).aspx +func getFileSystemType(path string) (fsType string, hr error) { + drive := filepath.VolumeName(path) + if len(drive) != 2 { + return "", errors.New("getFileSystemType path must start with a drive letter") + } + + var ( + modkernel32 = windows.NewLazySystemDLL("kernel32.dll") + procGetVolumeInformation = modkernel32.NewProc("GetVolumeInformationW") + buf = make([]uint16, 255) + size = windows.MAX_PATH + 1 + ) + drive += `\` + n := uintptr(unsafe.Pointer(nil)) + r0, _, _ := syscall.Syscall9(procGetVolumeInformation.Addr(), 8, uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(drive))), n, n, n, n, n, uintptr(unsafe.Pointer(&buf[0])), uintptr(size), 0) + if int32(r0) < 0 { + hr = syscall.Errno(win32FromHresult(r0)) + } + fsType = windows.UTF16ToString(buf) + return +} + +// win32FromHresult is a helper function to get the win32 error code from an HRESULT +func win32FromHresult(hr uintptr) uintptr { + if hr&0x1fff0000 == 0x00070000 { + return hr & 0xffff + } + return hr } diff --git a/sys/filesys_unix.go b/sys/filesys_unix.go new file mode 100644 index 000000000..ed23609c5 --- /dev/null +++ b/sys/filesys_unix.go @@ -0,0 +1,10 @@ +// +build !windows + +package sys + +import "os" + +// ForceRemoveAll on unix is just a wrapper for os.RemoveAll +func ForceRemoveAll(path string) error { + return os.RemoveAll(path) +} diff --git a/sys/filesys_windows.go b/sys/filesys_windows.go index b5ce13579..36395c556 100644 --- a/sys/filesys_windows.go +++ b/sys/filesys_windows.go @@ -11,6 +11,7 @@ import ( "unsafe" winio "github.com/Microsoft/go-winio" + "github.com/Microsoft/hcsshim" ) // MkdirAllWithACL is a wrapper for MkdirAll that creates a directory @@ -234,3 +235,13 @@ func syscallOpenSequential(path string, mode int, _ uint32) (fd syscall.Handle, h, e := syscall.CreateFile(pathp, access, sharemode, sa, createmode, fileFlagSequentialScan, 0) return h, e } + +// ForceRemoveAll is the same as os.RemoveAll, but uses hcsshim.DestroyLayer in order +// to delete container layers. +func ForceRemoveAll(path string) error { + info := hcsshim.DriverInfo{ + HomeDir: filepath.Dir(path), + } + + return hcsshim.DestroyLayer(info, filepath.Base(path)) +} diff --git a/windows/hcsshim.go b/windows/hcsshim.go index 7801da749..8d9501361 100644 --- a/windows/hcsshim.go +++ b/windows/hcsshim.go @@ -4,14 +4,12 @@ package windows import ( "context" - "fmt" "os" "path/filepath" "strings" "github.com/Microsoft/hcsshim" "github.com/containerd/containerd/errdefs" - "github.com/containerd/containerd/log" specs "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" ) @@ -49,14 +47,14 @@ func newWindowsContainerConfig(ctx context.Context, owner, id string, spec *spec } conf.IgnoreFlushesDuringBoot = spec.Windows.IgnoreFlushesDuringBoot - if len(spec.Windows.LayerFolders) < 1 { + if len(spec.Windows.LayerFolders) < 2 { return nil, errors.Wrap(errdefs.ErrInvalidArgument, - "spec.Windows.LayerFolders must have at least 1 layers") + "spec.Windows.LayerFolders must have at least 2 layers") } var ( - layerFolders = spec.Windows.LayerFolders - homeDir = filepath.Dir(layerFolders[0]) - layerFolderPath = filepath.Join(homeDir, id) + layerFolderPath = spec.Windows.LayerFolders[0] + layerFolders = spec.Windows.LayerFolders[1:] + layerID = filepath.Base(layerFolderPath) ) // TODO: use the create request Mount for those @@ -71,39 +69,12 @@ func newWindowsContainerConfig(ctx context.Context, owner, id string, spec *spec Path: layerPath, }) } - - var ( - di = hcsshim.DriverInfo{ - Flavour: 1, // filter driver - HomeDir: homeDir, - } - ) conf.LayerFolderPath = layerFolderPath - // TODO: Once there is a snapshotter for windows, this can be deleted. - // The R/W Layer should come from the Rootfs Mounts provided - // - // Windows doesn't support creating a container with a readonly - // filesystem, so always create a RW one - if err = hcsshim.CreateSandboxLayer(di, id, layerFolders[0], layerFolders); err != nil { - return nil, errors.Wrapf(err, "failed to create sandbox layer for %s: layers: %#v, driverInfo: %#v", - id, layerFolders, di) + var di = hcsshim.DriverInfo{ + HomeDir: filepath.Dir(layerFolderPath), } - defer func() { - if err != nil { - removeLayer(ctx, conf.LayerFolderPath) - } - }() - - if err = hcsshim.ActivateLayer(di, id); err != nil { - return nil, errors.Wrapf(err, "failed to activate layer %s", conf.LayerFolderPath) - } - - if err = hcsshim.PrepareLayer(di, id, layerFolders); err != nil { - return nil, errors.Wrapf(err, "failed to prepare layer %s", conf.LayerFolderPath) - } - - conf.VolumePath, err = hcsshim.GetLayerMountPath(di, id) + conf.VolumePath, err = hcsshim.GetLayerMountPath(di, layerID) if err != nil { return nil, errors.Wrapf(err, "failed to getmount path for layer %s: driverInfo: %#v", id, di) } @@ -146,41 +117,6 @@ func newWindowsContainerConfig(ctx context.Context, owner, id string, spec *spec return conf, nil } -// removeLayer deletes the given layer, all associated containers must have -// been shutdown for this to succeed. -func removeLayer(ctx context.Context, path string) error { - var ( - err error - layerID = filepath.Base(path) - parentPath = filepath.Dir(path) - di = hcsshim.DriverInfo{ - Flavour: 1, // filter driver - HomeDir: parentPath, - } - ) - - if err = hcsshim.UnprepareLayer(di, layerID); err != nil { - log.G(ctx).WithError(err).Warnf("failed to unprepare layer %s for removal", path) - } - - if err = hcsshim.DeactivateLayer(di, layerID); err != nil { - log.G(ctx).WithError(err).Warnf("failed to deactivate layer %s for removal", path) - } - - removePath := filepath.Join(parentPath, fmt.Sprintf("%s-removing", layerID)) - if err = os.Rename(path, removePath); err != nil { - log.G(ctx).WithError(err).Warnf("failed to rename container layer %s for removal", path) - removePath = path - } - - if err = hcsshim.DestroyLayer(di, removePath); err != nil { - log.G(ctx).WithError(err).Errorf("failed to remove container layer %s", removePath) - return err - } - - return nil -} - func newProcessConfig(processSpec *specs.Process, pset *pipeSet) *hcsshim.ProcessConfig { conf := &hcsshim.ProcessConfig{ EmulateConsole: pset.src.Terminal, diff --git a/windows/meta.go b/windows/meta.go deleted file mode 100644 index 81ab9245e..000000000 --- a/windows/meta.go +++ /dev/null @@ -1,54 +0,0 @@ -// +build windows - -package windows - -// TODO: remove this file (i.e. meta.go) once we have a snapshotter - -import ( - "github.com/boltdb/bolt" - "github.com/containerd/containerd/errdefs" - "github.com/pkg/errors" -) - -func newLayerFolderStore(tx *bolt.Tx) *layerFolderStore { - return &layerFolderStore{tx} -} - -type layerFolderStore struct { - tx *bolt.Tx -} - -func (s *layerFolderStore) Create(id, layer string) error { - bkt, err := s.tx.CreateBucketIfNotExists([]byte(pluginID)) - if err != nil { - return errors.Wrapf(err, "failed to create bucket %s", pluginID) - } - err = bkt.Put([]byte(id), []byte(layer)) - if err != nil { - return errors.Wrapf(err, "failed to store entry %s:%s", id, layer) - } - - return nil -} - -func (s *layerFolderStore) Get(id string) (string, error) { - bkt := s.tx.Bucket([]byte(pluginID)) - if bkt == nil { - return "", errors.Wrapf(errdefs.ErrNotFound, "bucket %s", pluginID) - } - - return string(bkt.Get([]byte(id))), nil -} - -func (s *layerFolderStore) Delete(id string) error { - bkt := s.tx.Bucket([]byte(pluginID)) - if bkt == nil { - return errors.Wrapf(errdefs.ErrNotFound, "bucket %s", pluginID) - } - - if err := bkt.Delete([]byte(id)); err != nil { - return errors.Wrapf(err, "failed to delete entry %s", id) - } - - return nil -} diff --git a/windows/runtime.go b/windows/runtime.go index fd26b270c..542c97363 100644 --- a/windows/runtime.go +++ b/windows/runtime.go @@ -10,13 +10,12 @@ import ( "time" "github.com/Microsoft/hcsshim" - "github.com/boltdb/bolt" eventstypes "github.com/containerd/containerd/api/events" - containerdtypes "github.com/containerd/containerd/api/types" + "github.com/containerd/containerd/api/types" "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/events" "github.com/containerd/containerd/log" - "github.com/containerd/containerd/metadata" + "github.com/containerd/containerd/mount" "github.com/containerd/containerd/namespaces" "github.com/containerd/containerd/plugin" "github.com/containerd/containerd/runtime" @@ -55,12 +54,6 @@ func New(ic *plugin.InitContext) (interface{}, error) { if err := os.MkdirAll(ic.Root, 0700); err != nil { return nil, errors.Wrapf(err, "could not create state directory at %s", ic.Root) } - - m, err := ic.Get(plugin.MetadataPlugin) - if err != nil { - return nil, err - } - r := &windowsRuntime{ root: ic.Root, pidPool: newPidPool(), @@ -70,7 +63,6 @@ func New(ic *plugin.InitContext) (interface{}, error) { // TODO(mlaventure): windows needs a stat monitor monitor: nil, tasks: runtime.NewTaskList(), - db: m.(*metadata.DB), } // Load our existing containers and kill/delete them. We don't support @@ -91,7 +83,6 @@ type windowsRuntime struct { monitor runtime.TaskMonitor tasks *runtime.TaskList - db *metadata.DB } func (r *windowsRuntime) ID() string { @@ -125,7 +116,17 @@ func (r *windowsRuntime) Create(ctx context.Context, id string, opts runtime.Cre createOpts.TerminateDuration = defaultTerminateDuration } - return r.newTask(ctx, namespace, id, spec, opts.IO, createOpts) + if len(opts.Rootfs) == 0 { + return nil, errors.Wrap(errdefs.ErrInvalidArgument, "rootfs was not provided to container create") + } + spec.Windows.LayerFolders = append(spec.Windows.LayerFolders, opts.Rootfs[0].Source) + parentLayerPaths, err := opts.Rootfs[0].GetParentPaths() + if err != nil { + return nil, err + } + spec.Windows.LayerFolders = append(spec.Windows.LayerFolders, parentLayerPaths...) + + return r.newTask(ctx, namespace, id, opts.Rootfs, spec, opts.IO, createOpts) } func (r *windowsRuntime) Get(ctx context.Context, id string) (runtime.Task, error) { @@ -209,14 +210,19 @@ func (r *windowsRuntime) Delete(ctx context.Context, t runtime.Task) (*runtime.E ns, _ := namespaces.Namespace(ctx) serviceCtx := log.WithLogger(context.Background(), log.GetLogger(ctx)) serviceCtx = namespaces.WithNamespace(serviceCtx, ns) - r.serviceTask(serviceCtx, ns, wt.id+"_servicing", wt.spec) + r.serviceTask(serviceCtx, ns, wt.id+"_servicing", wt.rootfs, wt.spec) + } + + if err := mount.UnmountAll(wt.rootfs[0].Source, 0); err != nil { + log.G(ctx).WithError(err).WithField("path", wt.rootfs[0].Source). + Warn("failed to unmount rootfs on failure") } // We were never started, return failure return rtExit, nil } -func (r *windowsRuntime) newTask(ctx context.Context, namespace, id string, spec *specs.Spec, io runtime.IO, createOpts *hcsshimtypes.CreateOptions) (*task, error) { +func (r *windowsRuntime) newTask(ctx context.Context, namespace, id string, rootfs []mount.Mount, spec *specs.Spec, io runtime.IO, createOpts *hcsshimtypes.CreateOptions) (*task, error) { var ( err error pset *pipeSet @@ -241,6 +247,18 @@ func (r *windowsRuntime) newTask(ctx context.Context, namespace, id string, spec } }() + if err := mount.All(rootfs, ""); err != nil { + return nil, errors.Wrap(err, "failed to mount rootfs") + } + defer func() { + if err != nil { + if err := mount.UnmountAll(rootfs[0].Source, 0); err != nil { + log.G(ctx).WithError(err).WithField("path", rootfs[0].Source). + Warn("failed to unmount rootfs on failure") + } + } + }() + var ( conf *hcsshim.ContainerConfig nsid = namespace + "-" + id @@ -248,31 +266,6 @@ func (r *windowsRuntime) newTask(ctx context.Context, namespace, id string, spec if conf, err = newWindowsContainerConfig(ctx, hcsshimOwner, nsid, spec); err != nil { return nil, err } - defer func() { - if err != nil { - removeLayer(ctx, conf.LayerFolderPath) - } - }() - - // TODO: remove this once we have a windows snapshotter - // Store the LayerFolder in the db so we can clean it if we die - if err = r.db.Update(func(tx *bolt.Tx) error { - s := newLayerFolderStore(tx) - return s.Create(nsid, conf.LayerFolderPath) - }); err != nil { - return nil, err - } - defer func() { - if err != nil { - if dbErr := r.db.Update(func(tx *bolt.Tx) error { - s := newLayerFolderStore(tx) - return s.Delete(nsid) - }); dbErr != nil { - log.G(ctx).WithField("id", id). - Error("failed to remove key from metadata") - } - } - }() ctr, err := hcsshim.CreateContainer(nsid, conf) if err != nil { @@ -301,6 +294,7 @@ func (r *windowsRuntime) newTask(ctx context.Context, namespace, id string, spec hyperV: spec.Windows.HyperV != nil, publisher: r.publisher, rwLayer: conf.LayerFolderPath, + rootfs: rootfs, pidPool: r.pidPool, hcsContainer: ctr, terminateDuration: createOpts.TerminateDuration, @@ -312,11 +306,12 @@ func (r *windowsRuntime) newTask(ctx context.Context, namespace, id string, spec } r.tasks.Add(ctx, t) - var rootfs []*containerdtypes.Mount - for _, l := range append([]string{t.rwLayer}, spec.Windows.LayerFolders...) { - rootfs = append(rootfs, &containerdtypes.Mount{ - Type: "windows-layer", - Source: l, + var eventRootfs []*types.Mount + for _, m := range rootfs { + eventRootfs = append(eventRootfs, &types.Mount{ + Type: m.Type, + Source: m.Source, + Options: m.Options, }) } @@ -331,7 +326,7 @@ func (r *windowsRuntime) newTask(ctx context.Context, namespace, id string, spec Terminal: io.Terminal, }, Pid: t.pid, - Rootfs: rootfs, + Rootfs: eventRootfs, // TODO: what should be in Bundle for windows? }) @@ -360,34 +355,10 @@ func (r *windowsRuntime) cleanup(ctx context.Context) { container.Wait() } container.Close() - - // TODO: remove this once we have a windows snapshotter - var layerFolderPath string - if err := r.db.View(func(tx *bolt.Tx) error { - s := newLayerFolderStore(tx) - l, e := s.Get(p.ID) - if err == nil { - layerFolderPath = l - } - return e - }); err == nil && layerFolderPath != "" { - removeLayer(ctx, layerFolderPath) - if dbErr := r.db.Update(func(tx *bolt.Tx) error { - s := newLayerFolderStore(tx) - return s.Delete(p.ID) - }); dbErr != nil { - log.G(ctx).WithField("id", p.ID). - Error("failed to remove key from metadata") - } - } else { - log.G(ctx).WithField("id", p.ID). - Debug("key not found in metadata, R/W layer may be leaked") - } - } } -func (r *windowsRuntime) serviceTask(ctx context.Context, namespace, id string, spec *specs.Spec) { +func (r *windowsRuntime) serviceTask(ctx context.Context, namespace, id string, rootfs []mount.Mount, spec *specs.Spec) { var ( err error t *task @@ -397,7 +368,7 @@ func (r *windowsRuntime) serviceTask(ctx context.Context, namespace, id string, } ) - t, err = r.newTask(ctx, namespace, id, spec, io, createOpts) + t, err = r.newTask(ctx, namespace, id, rootfs, spec, io, createOpts) if err != nil { log.G(ctx).WithError(err).WithField("id", id). Warn("failed to created servicing task") diff --git a/windows/task.go b/windows/task.go index 89bce1694..950f2e966 100644 --- a/windows/task.go +++ b/windows/task.go @@ -11,6 +11,7 @@ import ( eventstypes "github.com/containerd/containerd/api/events" "github.com/containerd/containerd/errdefs" "github.com/containerd/containerd/events" + "github.com/containerd/containerd/mount" "github.com/containerd/containerd/runtime" "github.com/containerd/containerd/windows/hcsshimtypes" "github.com/containerd/typeurl" @@ -33,6 +34,7 @@ type task struct { publisher events.Publisher rwLayer string + rootfs []mount.Mount pidPool *pidPool hcsContainer hcsshim.Container @@ -406,6 +408,5 @@ func (t *task) cleanup() { for _, p := range t.processes { t.removeProcessNL(p.id) } - removeLayer(context.Background(), t.rwLayer) t.Unlock() }