diff --git a/api/common.go b/api/common.go index 142b76966c..61a3692de1 100644 --- a/api/common.go +++ b/api/common.go @@ -126,7 +126,7 @@ func MatchesContentType(contentType, expectedType string) bool { // LoadOrCreateTrustKey attempts to load the libtrust key at the given path, // otherwise generates a new one func LoadOrCreateTrustKey(trustKeyPath string) (libtrust.PrivateKey, error) { - err := system.MkdirAll(filepath.Dir(trustKeyPath), 0700) + err := system.MkdirAll(filepath.Dir(trustKeyPath), 0700, "") if err != nil { return nil, err } diff --git a/api/server/backend/build/backend.go b/api/server/backend/build/backend.go index 00aa7c3126..98051653cf 100644 --- a/api/server/backend/build/backend.go +++ b/api/server/backend/build/backend.go @@ -17,7 +17,7 @@ import ( // ImageComponent provides an interface for working with images type ImageComponent interface { SquashImage(from string, to string) (string, error) - TagImageWithReference(image.ID, reference.Named) error + TagImageWithReference(image.ID, string, reference.Named) error } // Backend provides build functionality to the API router diff --git a/api/server/backend/build/tag.go b/api/server/backend/build/tag.go index 5c3918a3e1..e2de4c13bb 100644 --- a/api/server/backend/build/tag.go +++ b/api/server/backend/build/tag.go @@ -3,9 +3,11 @@ package build import ( "fmt" "io" + "runtime" "github.com/docker/distribution/reference" "github.com/docker/docker/image" + "github.com/docker/docker/pkg/system" "github.com/pkg/errors" ) @@ -33,7 +35,12 @@ func NewTagger(backend ImageComponent, stdout io.Writer, names []string) (*Tagge // TagImages creates image tags for the imageID func (bt *Tagger) TagImages(imageID image.ID) error { for _, rt := range bt.repoAndTags { - if err := bt.imageComponent.TagImageWithReference(imageID, rt); err != nil { + // TODO @jhowardmsft LCOW support. Will need revisiting. + platform := runtime.GOOS + if platform == "windows" && system.LCOWSupported() { + platform = "linux" + } + if err := bt.imageComponent.TagImageWithReference(imageID, platform, rt); err != nil { return err } fmt.Fprintf(bt.stdout, "Successfully tagged %s\n", reference.FamiliarString(rt)) diff --git a/api/server/router/image/backend.go b/api/server/router/image/backend.go index 3b9ed96147..9a588a71a9 100644 --- a/api/server/router/image/backend.go +++ b/api/server/router/image/backend.go @@ -35,12 +35,12 @@ type imageBackend interface { type importExportBackend interface { LoadImage(inTar io.ReadCloser, outStream io.Writer, quiet bool) error - ImportImage(src string, repository, tag string, msg string, inConfig io.ReadCloser, outStream io.Writer, changes []string) error + ImportImage(src string, repository, platform string, tag string, msg string, inConfig io.ReadCloser, outStream io.Writer, changes []string) error ExportImage(names []string, outStream io.Writer) error } type registryBackend interface { - PullImage(ctx context.Context, image, tag string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error + PullImage(ctx context.Context, image, tag, platform string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error PushImage(ctx context.Context, image, tag string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error SearchRegistryForImages(ctx context.Context, filtersArgs string, term string, limit int, authConfig *types.AuthConfig, metaHeaders map[string][]string) (*registry.SearchResults, error) } diff --git a/api/server/router/image/image_routes.go b/api/server/router/image/image_routes.go index 465182caa1..232d795683 100644 --- a/api/server/router/image/image_routes.go +++ b/api/server/router/image/image_routes.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "net/http" + "runtime" "strconv" "strings" @@ -17,6 +18,7 @@ import ( "github.com/docker/docker/api/types/versions" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/streamformatter" + "github.com/docker/docker/pkg/system" "github.com/docker/docker/registry" "golang.org/x/net/context" ) @@ -85,6 +87,41 @@ func (s *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWrite ) defer output.Close() + // TODO @jhowardmsft LCOW Support: Eventually we will need an API change + // so that platform comes from (for example) r.Form.Get("platform"). For + // the initial implementation, we assume that the platform is the + // runtime OS of the host. It will also need a validation function such + // as below which should be called after getting it from the API. + // + // Ensures the requested platform is valid and normalized + //func validatePlatform(req string) (string, error) { + // req = strings.ToLower(req) + // if req == "" { + // req = runtime.GOOS // default to host platform + // } + // valid := []string{runtime.GOOS} + // + // if runtime.GOOS == "windows" && system.LCOWSupported() { + // valid = append(valid, "linux") + // } + // + // for _, item := range valid { + // if req == item { + // return req, nil + // } + // } + // return "", fmt.Errorf("invalid platform requested: %s", req) + //} + // + // And in the call-site: + // if platform, err = validatePlatform(platform); err != nil { + // return err + // } + platform := runtime.GOOS + if platform == "windows" && system.LCOWSupported() { + platform = "linux" + } + w.Header().Set("Content-Type", "application/json") if image != "" { //pull @@ -106,13 +143,13 @@ func (s *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWrite } } - err = s.backend.PullImage(ctx, image, tag, metaHeaders, authConfig, output) + err = s.backend.PullImage(ctx, image, tag, platform, metaHeaders, authConfig, output) } else { //import src := r.Form.Get("fromSrc") // 'err' MUST NOT be defined within this block, we need any error // generated from the download to be available to the output // stream processing below - err = s.backend.ImportImage(src, repo, tag, message, r.Body, output, r.Form["changes"]) + err = s.backend.ImportImage(src, repo, platform, tag, message, r.Body, output, r.Form["changes"]) } if err != nil { if !output.Flushed() { diff --git a/api/types/backend/build.go b/api/types/backend/build.go index db3b26f070..300d358969 100644 --- a/api/types/backend/build.go +++ b/api/types/backend/build.go @@ -40,4 +40,5 @@ type GetImageAndLayerOptions struct { PullOption PullOption AuthConfig map[string]types.AuthConfig Output io.Writer + Platform string } diff --git a/api/types/client.go b/api/types/client.go index 0ce2c94303..1e11872fb2 100644 --- a/api/types/client.go +++ b/api/types/client.go @@ -178,6 +178,10 @@ type ImageBuildOptions struct { SecurityOpt []string ExtraHosts []string // List of extra hosts Target string + + // TODO @jhowardmsft LCOW Support: This will require extending to include + // `Platform string`, but is ommited for now as it's hard-coded temporarily + // to avoid API changes. } // ImageBuildResponse holds information diff --git a/api/types/configs.go b/api/types/configs.go index 20c19f2132..e4d2ce6e36 100644 --- a/api/types/configs.go +++ b/api/types/configs.go @@ -16,6 +16,7 @@ type ContainerCreateConfig struct { HostConfig *container.HostConfig NetworkingConfig *network.NetworkingConfig AdjustCPUShares bool + Platform string } // ContainerRmConfig holds arguments for the container remove diff --git a/api/types/types.go b/api/types/types.go index 37adca2f57..8f309b2100 100644 --- a/api/types/types.go +++ b/api/types/types.go @@ -320,6 +320,7 @@ type ContainerJSONBase struct { Name string RestartCount int Driver string + Platform string MountLabel string ProcessLabel string AppArmorProfile string diff --git a/builder/builder.go b/builder/builder.go index 5cd6c838f9..e480601d46 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -43,7 +43,7 @@ type Backend interface { // ContainerCreateWorkdir creates the workdir ContainerCreateWorkdir(containerID string) error - CreateImage(config []byte, parent string) (Image, error) + CreateImage(config []byte, parent string, platform string) (Image, error) ImageCacheBuilder } @@ -78,7 +78,7 @@ type Result struct { // ImageCacheBuilder represents a generator for stateful image cache. type ImageCacheBuilder interface { // MakeImageCache creates a stateful image cache. - MakeImageCache(cacheFrom []string) ImageCache + MakeImageCache(cacheFrom []string, platform string) ImageCache } // ImageCache abstracts an image cache. @@ -100,6 +100,6 @@ type Image interface { type ReleaseableLayer interface { Release() error Mount() (string, error) - Commit() (ReleaseableLayer, error) + Commit(platform string) (ReleaseableLayer, error) DiffID() layer.DiffID } diff --git a/builder/dockerfile/builder.go b/builder/dockerfile/builder.go index d35ed9b91a..86f8f450af 100644 --- a/builder/dockerfile/builder.go +++ b/builder/dockerfile/builder.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "io/ioutil" + "runtime" "strings" "github.com/Sirupsen/logrus" @@ -20,6 +21,7 @@ import ( "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/streamformatter" "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/system" "github.com/pkg/errors" "golang.org/x/net/context" "golang.org/x/sync/syncmap" @@ -73,13 +75,24 @@ func (bm *BuildManager) Build(ctx context.Context, config backend.BuildConfig) ( }() } + // TODO @jhowardmsft LCOW support - this will require rework to allow both linux and Windows simultaneously. + // This is an interim solution to hardcode to linux if LCOW is turned on. + if dockerfile.Platform == "" { + dockerfile.Platform = runtime.GOOS + if dockerfile.Platform == "windows" && system.LCOWSupported() { + dockerfile.Platform = "linux" + } + } + builderOptions := builderOptions{ Options: config.Options, ProgressWriter: config.ProgressWriter, Backend: bm.backend, PathCache: bm.pathCache, Archiver: bm.archiver, + Platform: dockerfile.Platform, } + return newBuilder(ctx, builderOptions).build(source, dockerfile) } @@ -90,6 +103,7 @@ type builderOptions struct { ProgressWriter backend.ProgressWriter PathCache pathCache Archiver *archive.Archiver + Platform string } // Builder is a Dockerfile builder @@ -113,14 +127,32 @@ type Builder struct { pathCache pathCache containerManager *containerManager imageProber ImageProber + + // TODO @jhowardmft LCOW Support. This will be moved to options at a later + // stage, however that cannot be done now as it affects the public API + // if it were. + platform string } // newBuilder creates a new Dockerfile builder from an optional dockerfile and a Options. +// TODO @jhowardmsft LCOW support: Eventually platform can be moved into the builder +// options, however, that would be an API change as it shares types.ImageBuildOptions. func newBuilder(clientCtx context.Context, options builderOptions) *Builder { config := options.Options if config == nil { config = new(types.ImageBuildOptions) } + + // @jhowardmsft LCOW Support. For the time being, this is interim. Eventually + // will be moved to types.ImageBuildOptions, but it can't for now as that would + // be an API change. + if options.Platform == "" { + options.Platform = runtime.GOOS + } + if options.Platform == "windows" && system.LCOWSupported() { + options.Platform = "linux" + } + b := &Builder{ clientCtx: clientCtx, options: config, @@ -134,9 +166,11 @@ func newBuilder(clientCtx context.Context, options builderOptions) *Builder { buildStages: newBuildStages(), imageSources: newImageSources(clientCtx, options), pathCache: options.PathCache, - imageProber: newImageProber(options.Backend, config.CacheFrom, config.NoCache), + imageProber: newImageProber(options.Backend, config.CacheFrom, options.Platform, config.NoCache), containerManager: newContainerManager(options.Backend), + platform: options.Platform, } + return b } @@ -267,6 +301,17 @@ func BuildFromConfig(config *container.Config, changes []string) (*container.Con return nil, err } + // TODO @jhowardmsft LCOW support. For now, if LCOW enabled, switch to linux. + // Also explicitly set the platform. Ultimately this will be in the builder + // options, but we can't do that yet as it would change the API. + if dockerfile.Platform == "" { + dockerfile.Platform = runtime.GOOS + } + if dockerfile.Platform == "windows" && system.LCOWSupported() { + dockerfile.Platform = "linux" + } + b.platform = dockerfile.Platform + // ensure that the commands are valid for _, n := range dockerfile.AST.Children { if !validCommitCommands[n.Value] { diff --git a/builder/dockerfile/builder_unix.go b/builder/dockerfile/builder_unix.go index 76a7ce74f9..5ea63da824 100644 --- a/builder/dockerfile/builder_unix.go +++ b/builder/dockerfile/builder_unix.go @@ -2,4 +2,6 @@ package dockerfile -var defaultShell = []string{"/bin/sh", "-c"} +func defaultShellForPlatform(platform string) []string { + return []string{"/bin/sh", "-c"} +} diff --git a/builder/dockerfile/builder_windows.go b/builder/dockerfile/builder_windows.go index 37e9fbcf4b..7bfef3238b 100644 --- a/builder/dockerfile/builder_windows.go +++ b/builder/dockerfile/builder_windows.go @@ -1,3 +1,8 @@ package dockerfile -var defaultShell = []string{"cmd", "/S", "/C"} +func defaultShellForPlatform(platform string) []string { + if platform == "linux" { + return []string{"/bin/sh", "-c"} + } + return []string{"cmd", "/S", "/C"} +} diff --git a/builder/dockerfile/containerbackend.go b/builder/dockerfile/containerbackend.go index aa8001ae60..7b241f3d3b 100644 --- a/builder/dockerfile/containerbackend.go +++ b/builder/dockerfile/containerbackend.go @@ -28,10 +28,11 @@ func newContainerManager(docker builder.ExecBackend) *containerManager { } // Create a container -func (c *containerManager) Create(runConfig *container.Config, hostConfig *container.HostConfig) (container.ContainerCreateCreatedBody, error) { +func (c *containerManager) Create(runConfig *container.Config, hostConfig *container.HostConfig, platform string) (container.ContainerCreateCreatedBody, error) { container, err := c.backend.ContainerCreate(types.ContainerCreateConfig{ Config: runConfig, HostConfig: hostConfig, + Platform: platform, }) if err != nil { return container, err diff --git a/builder/dockerfile/dispatchers.go b/builder/dockerfile/dispatchers.go index cc6d0990dc..1f74241248 100644 --- a/builder/dockerfile/dispatchers.go +++ b/builder/dockerfile/dispatchers.go @@ -26,6 +26,7 @@ import ( "github.com/docker/docker/image" "github.com/docker/docker/pkg/jsonmessage" "github.com/docker/docker/pkg/signal" + "github.com/docker/docker/pkg/system" "github.com/docker/go-connections/nat" "github.com/pkg/errors" ) @@ -274,10 +275,12 @@ func (b *Builder) getFromImage(shlex *ShellLex, name string) (builder.Image, err localOnly = true } - // Windows cannot support a container with no base image. + // Windows cannot support a container with no base image unless it is LCOW. if name == api.NoBaseImageSpecifier { if runtime.GOOS == "windows" { - return nil, errors.New("Windows does not support FROM scratch") + if b.platform == "windows" || (b.platform != "windows" && !system.LCOWSupported()) { + return nil, errors.New("Windows does not support FROM scratch") + } } return scratchImage, nil } @@ -400,7 +403,7 @@ func workdir(req dispatchRequest) error { } comment := "WORKDIR " + runConfig.WorkingDir - runConfigWithCommentCmd := copyRunConfig(runConfig, withCmdCommentString(comment)) + runConfigWithCommentCmd := copyRunConfig(runConfig, withCmdCommentString(comment, req.builder.platform)) containerID, err := req.builder.probeAndCreate(req.state, runConfigWithCommentCmd) if err != nil || containerID == "" { return err @@ -418,7 +421,7 @@ func workdir(req dispatchRequest) error { // the current SHELL which defaults to 'sh -c' under linux or 'cmd /S /C' under // Windows, in the event there is only one argument The difference in processing: // -// RUN echo hi # sh -c echo hi (Linux) +// RUN echo hi # sh -c echo hi (Linux and LCOW) // RUN echo hi # cmd /S /C echo hi (Windows) // RUN [ "echo", "hi" ] # echo hi // @@ -434,7 +437,7 @@ func run(req dispatchRequest) error { stateRunConfig := req.state.runConfig args := handleJSONArgs(req.args, req.attributes) if !req.attributes["json"] { - args = append(getShell(stateRunConfig), args...) + args = append(getShell(stateRunConfig, req.builder.platform), args...) } cmdFromArgs := strslice.StrSlice(args) buildArgs := req.builder.buildArgs.FilterAllowed(stateRunConfig.Env) @@ -519,7 +522,7 @@ func cmd(req dispatchRequest) error { runConfig := req.state.runConfig cmdSlice := handleJSONArgs(req.args, req.attributes) if !req.attributes["json"] { - cmdSlice = append(getShell(runConfig), cmdSlice...) + cmdSlice = append(getShell(runConfig, req.builder.platform), cmdSlice...) } runConfig.Cmd = strslice.StrSlice(cmdSlice) @@ -671,7 +674,7 @@ func entrypoint(req dispatchRequest) error { runConfig.Entrypoint = nil default: // ENTRYPOINT echo hi - runConfig.Entrypoint = strslice.StrSlice(append(getShell(runConfig), parsed[0])) + runConfig.Entrypoint = strslice.StrSlice(append(getShell(runConfig, req.builder.platform), parsed[0])) } // when setting the entrypoint if a CMD was not explicitly set then diff --git a/builder/dockerfile/dispatchers_test.go b/builder/dockerfile/dispatchers_test.go index 91d00758d4..b3672fce1b 100644 --- a/builder/dockerfile/dispatchers_test.go +++ b/builder/dockerfile/dispatchers_test.go @@ -63,7 +63,7 @@ func newBuilderWithMockBackend() *Builder { Backend: mockBackend, }), buildStages: newBuildStages(), - imageProber: newImageProber(mockBackend, nil, false), + imageProber: newImageProber(mockBackend, nil, runtime.GOOS, false), containerManager: newContainerManager(mockBackend), } return b @@ -194,7 +194,7 @@ func TestFromScratch(t *testing.T) { req := defaultDispatchReq(b, "scratch") err := from(req) - if runtime.GOOS == "windows" { + if runtime.GOOS == "windows" && !system.LCOWSupported() { assert.EqualError(t, err, "Windows does not support FROM scratch") return } @@ -202,7 +202,12 @@ func TestFromScratch(t *testing.T) { require.NoError(t, err) assert.True(t, req.state.hasFromImage()) assert.Equal(t, "", req.state.imageID) - assert.Equal(t, []string{"PATH=" + system.DefaultPathEnv}, req.state.runConfig.Env) + // Windows does not set the default path. TODO @jhowardmsft LCOW support. This will need revisiting as we get further into the implementation + expected := "PATH=" + system.DefaultPathEnv(runtime.GOOS) + if runtime.GOOS == "windows" { + expected = "" + } + assert.Equal(t, []string{expected}, req.state.runConfig.Env) } func TestFromWithArg(t *testing.T) { @@ -469,7 +474,7 @@ func TestRunWithBuildArgs(t *testing.T) { runConfig := &container.Config{} origCmd := strslice.StrSlice([]string{"cmd", "in", "from", "image"}) - cmdWithShell := strslice.StrSlice(append(getShell(runConfig), "echo foo")) + cmdWithShell := strslice.StrSlice(append(getShell(runConfig, runtime.GOOS), "echo foo")) envVars := []string{"|1", "one=two"} cachedCmd := strslice.StrSlice(append(envVars, cmdWithShell...)) @@ -483,10 +488,10 @@ func TestRunWithBuildArgs(t *testing.T) { } mockBackend := b.docker.(*MockBackend) - mockBackend.makeImageCacheFunc = func(_ []string) builder.ImageCache { + mockBackend.makeImageCacheFunc = func(_ []string, _ string) builder.ImageCache { return imageCache } - b.imageProber = newImageProber(mockBackend, nil, false) + b.imageProber = newImageProber(mockBackend, nil, runtime.GOOS, false) mockBackend.getImageFunc = func(_ string) (builder.Image, builder.ReleaseableLayer, error) { return &mockImage{ id: "abcdef", diff --git a/builder/dockerfile/evaluator.go b/builder/dockerfile/evaluator.go index 6ee4f2cfce..69e3f6d0f2 100644 --- a/builder/dockerfile/evaluator.go +++ b/builder/dockerfile/evaluator.go @@ -22,6 +22,7 @@ package dockerfile import ( "bytes" "fmt" + "runtime" "strings" "github.com/docker/docker/api/types/container" @@ -228,14 +229,19 @@ func (s *dispatchState) beginStage(stageName string, image builder.Image) { } // Add the default PATH to runConfig.ENV if one exists for the platform and there -// is no PATH set. Note that windows won't have one as it's set by HCS +// is no PATH set. Note that Windows containers on Windows won't have one as it's set by HCS func (s *dispatchState) setDefaultPath() { - if system.DefaultPathEnv == "" { + // TODO @jhowardmsft LCOW Support - This will need revisiting later + platform := runtime.GOOS + if platform == "windows" && system.LCOWSupported() { + platform = "linux" + } + if system.DefaultPathEnv(platform) == "" { return } envMap := opts.ConvertKVStringsToMap(s.runConfig.Env) if _, ok := envMap["PATH"]; !ok { - s.runConfig.Env = append(s.runConfig.Env, "PATH="+system.DefaultPathEnv) + s.runConfig.Env = append(s.runConfig.Env, "PATH="+system.DefaultPathEnv(platform)) } } diff --git a/builder/dockerfile/imagecontext.go b/builder/dockerfile/imagecontext.go index 4e2a40fc1a..64b2572b85 100644 --- a/builder/dockerfile/imagecontext.go +++ b/builder/dockerfile/imagecontext.go @@ -97,6 +97,8 @@ type imageSources struct { cache pathCache // TODO: remove } +// TODO @jhowardmsft LCOW Support: Eventually, platform can be moved to options.Options.Platform, +// and removed from builderOptions, but that can't be done yet as it would affect the API. func newImageSources(ctx context.Context, options builderOptions) *imageSources { getAndMount := func(idOrRef string, localOnly bool) (builder.Image, builder.ReleaseableLayer, error) { pullOption := backend.PullOptionNoPull @@ -111,6 +113,7 @@ func newImageSources(ctx context.Context, options builderOptions) *imageSources PullOption: pullOption, AuthConfig: options.Options.AuthConfigs, Output: options.ProgressWriter.Output, + Platform: options.Platform, }) } diff --git a/builder/dockerfile/imageprobe.go b/builder/dockerfile/imageprobe.go index 3a3942888c..3433612de5 100644 --- a/builder/dockerfile/imageprobe.go +++ b/builder/dockerfile/imageprobe.go @@ -19,13 +19,13 @@ type imageProber struct { cacheBusted bool } -func newImageProber(cacheBuilder builder.ImageCacheBuilder, cacheFrom []string, noCache bool) ImageProber { +func newImageProber(cacheBuilder builder.ImageCacheBuilder, cacheFrom []string, platform string, noCache bool) ImageProber { if noCache { return &nopProber{} } reset := func() builder.ImageCache { - return cacheBuilder.MakeImageCache(cacheFrom) + return cacheBuilder.MakeImageCache(cacheFrom, platform) } return &imageProber{cache: reset(), reset: reset} } diff --git a/builder/dockerfile/internals.go b/builder/dockerfile/internals.go index 6f232df3db..c0d6081d06 100644 --- a/builder/dockerfile/internals.go +++ b/builder/dockerfile/internals.go @@ -25,7 +25,7 @@ func (b *Builder) commit(dispatchState *dispatchState, comment string) error { return errors.New("Please provide a source image with `from` prior to commit") } - runConfigWithCommentCmd := copyRunConfig(dispatchState.runConfig, withCmdComment(comment)) + runConfigWithCommentCmd := copyRunConfig(dispatchState.runConfig, withCmdComment(comment, b.platform)) hit, err := b.probeCache(dispatchState, runConfigWithCommentCmd) if err != nil || hit { return err @@ -65,7 +65,7 @@ func (b *Builder) commitContainer(dispatchState *dispatchState, id string, conta } func (b *Builder) exportImage(state *dispatchState, imageMount *imageMount, runConfig *container.Config) error { - newLayer, err := imageMount.Layer().Commit() + newLayer, err := imageMount.Layer().Commit(b.platform) if err != nil { return err } @@ -84,7 +84,7 @@ func (b *Builder) exportImage(state *dispatchState, imageMount *imageMount, runC ContainerConfig: runConfig, DiffID: newLayer.DiffID(), Config: copyRunConfig(state.runConfig), - }) + }, parentImage.OS) // TODO: it seems strange to marshal this here instead of just passing in the // image struct @@ -93,7 +93,7 @@ func (b *Builder) exportImage(state *dispatchState, imageMount *imageMount, runC return errors.Wrap(err, "failed to encode image config") } - exportedImage, err := b.docker.CreateImage(config, state.imageID) + exportedImage, err := b.docker.CreateImage(config, state.imageID, parentImage.OS) if err != nil { return errors.Wrapf(err, "failed to export image") } @@ -110,7 +110,7 @@ func (b *Builder) performCopy(state *dispatchState, inst copyInstruction) error // TODO: should this have been using origPaths instead of srcHash in the comment? runConfigWithCommentCmd := copyRunConfig( state.runConfig, - withCmdCommentString(fmt.Sprintf("%s %s in %s ", inst.cmdName, srcHash, inst.dest))) + withCmdCommentString(fmt.Sprintf("%s %s in %s ", inst.cmdName, srcHash, inst.dest), b.platform)) hit, err := b.probeCache(state, runConfigWithCommentCmd) if err != nil || hit { return err @@ -190,9 +190,9 @@ func withCmd(cmd []string) runConfigModifier { // withCmdComment sets Cmd to a nop comment string. See withCmdCommentString for // why there are two almost identical versions of this. -func withCmdComment(comment string) runConfigModifier { +func withCmdComment(comment string, platform string) runConfigModifier { return func(runConfig *container.Config) { - runConfig.Cmd = append(getShell(runConfig), "#(nop) ", comment) + runConfig.Cmd = append(getShell(runConfig, platform), "#(nop) ", comment) } } @@ -200,9 +200,9 @@ func withCmdComment(comment string) runConfigModifier { // A few instructions (workdir, copy, add) used a nop comment that is a single arg // where as all the other instructions used a two arg comment string. This // function implements the single arg version. -func withCmdCommentString(comment string) runConfigModifier { +func withCmdCommentString(comment string, platform string) runConfigModifier { return func(runConfig *container.Config) { - runConfig.Cmd = append(getShell(runConfig), "#(nop) "+comment) + runConfig.Cmd = append(getShell(runConfig, platform), "#(nop) "+comment) } } @@ -229,9 +229,9 @@ func withEntrypointOverride(cmd []string, entrypoint []string) runConfigModifier // getShell is a helper function which gets the right shell for prefixing the // shell-form of RUN, ENTRYPOINT and CMD instructions -func getShell(c *container.Config) []string { +func getShell(c *container.Config, platform string) []string { if 0 == len(c.Shell) { - return append([]string{}, defaultShell[:]...) + return append([]string{}, defaultShellForPlatform(platform)[:]...) } return append([]string{}, c.Shell[:]...) } @@ -256,13 +256,13 @@ func (b *Builder) probeAndCreate(dispatchState *dispatchState, runConfig *contai } // Set a log config to override any default value set on the daemon hostConfig := &container.HostConfig{LogConfig: defaultLogConfig} - container, err := b.containerManager.Create(runConfig, hostConfig) + container, err := b.containerManager.Create(runConfig, hostConfig, b.platform) return container.ID, err } func (b *Builder) create(runConfig *container.Config) (string, error) { hostConfig := hostConfigFromOptions(b.options) - container, err := b.containerManager.Create(runConfig, hostConfig) + container, err := b.containerManager.Create(runConfig, hostConfig, b.platform) if err != nil { return "", err } diff --git a/builder/dockerfile/internals_test.go b/builder/dockerfile/internals_test.go index 4cba2c9842..8073cc6713 100644 --- a/builder/dockerfile/internals_test.go +++ b/builder/dockerfile/internals_test.go @@ -2,6 +2,7 @@ package dockerfile import ( "fmt" + "runtime" "testing" "github.com/docker/docker/api/types" @@ -97,9 +98,9 @@ func TestCopyRunConfig(t *testing.T) { }, { doc: "Set the command to a comment", - modifiers: []runConfigModifier{withCmdComment("comment")}, + modifiers: []runConfigModifier{withCmdComment("comment", runtime.GOOS)}, expected: &container.Config{ - Cmd: append(defaultShell, "#(nop) ", "comment"), + Cmd: append(defaultShellForPlatform(runtime.GOOS), "#(nop) ", "comment"), Env: defaultEnv, }, }, diff --git a/builder/dockerfile/mockbackend_test.go b/builder/dockerfile/mockbackend_test.go index 876bb91f1b..adc22762e0 100644 --- a/builder/dockerfile/mockbackend_test.go +++ b/builder/dockerfile/mockbackend_test.go @@ -18,7 +18,7 @@ type MockBackend struct { containerCreateFunc func(config types.ContainerCreateConfig) (container.ContainerCreateCreatedBody, error) commitFunc func(string, *backend.ContainerCommitConfig) (string, error) getImageFunc func(string) (builder.Image, builder.ReleaseableLayer, error) - makeImageCacheFunc func(cacheFrom []string) builder.ImageCache + makeImageCacheFunc func(cacheFrom []string, platform string) builder.ImageCache } func (m *MockBackend) ContainerAttachRaw(cID string, stdin io.ReadCloser, stdout, stderr io.Writer, stream bool, attached chan struct{}) error { @@ -71,14 +71,14 @@ func (m *MockBackend) GetImageAndReleasableLayer(ctx context.Context, refOrID st return &mockImage{id: "theid"}, &mockLayer{}, nil } -func (m *MockBackend) MakeImageCache(cacheFrom []string) builder.ImageCache { +func (m *MockBackend) MakeImageCache(cacheFrom []string, platform string) builder.ImageCache { if m.makeImageCacheFunc != nil { - return m.makeImageCacheFunc(cacheFrom) + return m.makeImageCacheFunc(cacheFrom, platform) } return nil } -func (m *MockBackend) CreateImage(config []byte, parent string) (builder.Image, error) { +func (m *MockBackend) CreateImage(config []byte, parent string, platform string) (builder.Image, error) { return nil, nil } @@ -121,7 +121,7 @@ func (l *mockLayer) Mount() (string, error) { return "mountPath", nil } -func (l *mockLayer) Commit() (builder.ReleaseableLayer, error) { +func (l *mockLayer) Commit(string) (builder.ReleaseableLayer, error) { return nil, nil } diff --git a/builder/dockerfile/parser/parser.go b/builder/dockerfile/parser/parser.go index fa57d3953c..a1f7a74570 100644 --- a/builder/dockerfile/parser/parser.go +++ b/builder/dockerfile/parser/parser.go @@ -7,11 +7,13 @@ import ( "fmt" "io" "regexp" + "runtime" "strconv" "strings" "unicode" "github.com/docker/docker/builder/dockerfile/command" + "github.com/docker/docker/pkg/system" "github.com/pkg/errors" ) @@ -79,22 +81,28 @@ func (node *Node) AddChild(child *Node, startLine, endLine int) { } var ( - dispatch map[string]func(string, *Directive) (*Node, map[string]bool, error) - tokenWhitespace = regexp.MustCompile(`[\t\v\f\r ]+`) - tokenEscapeCommand = regexp.MustCompile(`^#[ \t]*escape[ \t]*=[ \t]*(?P.).*$`) - tokenComment = regexp.MustCompile(`^#.*$`) + dispatch map[string]func(string, *Directive) (*Node, map[string]bool, error) + tokenWhitespace = regexp.MustCompile(`[\t\v\f\r ]+`) + tokenEscapeCommand = regexp.MustCompile(`^#[ \t]*escape[ \t]*=[ \t]*(?P.).*$`) + tokenPlatformCommand = regexp.MustCompile(`^#[ \t]*platform[ \t]*=[ \t]*(?P.*)$`) + tokenComment = regexp.MustCompile(`^#.*$`) ) // DefaultEscapeToken is the default escape token const DefaultEscapeToken = '\\' +// DefaultPlatformToken is the platform assumed for the build if not explicitly provided +var DefaultPlatformToken = runtime.GOOS + // Directive is the structure used during a build run to hold the state of // parsing directives. type Directive struct { escapeToken rune // Current escape token + platformToken string // Current platform token lineContinuationRegex *regexp.Regexp // Current line continuation regex processingComplete bool // Whether we are done looking for directives escapeSeen bool // Whether the escape directive has been seen + platformSeen bool // Whether the platform directive has been seen } // setEscapeToken sets the default token for escaping characters in a Dockerfile. @@ -107,6 +115,22 @@ func (d *Directive) setEscapeToken(s string) error { return nil } +// setPlatformToken sets the default platform for pulling images in a Dockerfile. +func (d *Directive) setPlatformToken(s string) error { + s = strings.ToLower(s) + valid := []string{runtime.GOOS} + if runtime.GOOS == "windows" && system.LCOWSupported() { + valid = append(valid, "linux") + } + for _, item := range valid { + if s == item { + d.platformToken = s + return nil + } + } + return fmt.Errorf("invalid PLATFORM '%s'. Must be one of %v", s, valid) +} + // possibleParserDirective looks for one or more parser directives '# escapeToken=' and // '# platform='. Parser directives must precede any builder instruction // or other comments, and cannot be repeated. @@ -128,6 +152,23 @@ func (d *Directive) possibleParserDirective(line string) error { } } + // TODO @jhowardmsft LCOW Support: Eventually this check can be removed, + // but only recognise a platform token if running in LCOW mode. + if runtime.GOOS == "windows" && system.LCOWSupported() { + tpcMatch := tokenPlatformCommand.FindStringSubmatch(strings.ToLower(line)) + if len(tpcMatch) != 0 { + for i, n := range tokenPlatformCommand.SubexpNames() { + if n == "platform" { + if d.platformSeen == true { + return errors.New("only one platform parser directive can be used") + } + d.platformSeen = true + return d.setPlatformToken(tpcMatch[i]) + } + } + } + } + d.processingComplete = true return nil } @@ -136,6 +177,7 @@ func (d *Directive) possibleParserDirective(line string) error { func NewDefaultDirective() *Directive { directive := Directive{} directive.setEscapeToken(string(DefaultEscapeToken)) + directive.setPlatformToken(runtime.GOOS) return &directive } @@ -200,6 +242,7 @@ func newNodeFromLine(line string, directive *Directive) (*Node, error) { type Result struct { AST *Node EscapeToken rune + Platform string } // Parse reads lines from a Reader, parses the lines into an AST and returns @@ -252,7 +295,7 @@ func Parse(rwc io.Reader) (*Result, error) { } root.AddChild(child, startLine, currentLine) } - return &Result{AST: root, EscapeToken: d.escapeToken}, nil + return &Result{AST: root, EscapeToken: d.escapeToken, Platform: d.platformToken}, nil } func trimComments(src []byte) []byte { diff --git a/cmd/dockerd/config.go b/cmd/dockerd/config.go index da4071e46c..440497bfbc 100644 --- a/cmd/dockerd/config.go +++ b/cmd/dockerd/config.go @@ -1,6 +1,8 @@ package main import ( + "runtime" + "github.com/docker/docker/daemon/config" "github.com/docker/docker/opts" "github.com/spf13/pflag" @@ -33,7 +35,12 @@ func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) { flags.BoolVarP(&conf.AutoRestart, "restart", "r", true, "--restart on the daemon has been deprecated in favor of --restart policies on docker run") flags.MarkDeprecated("restart", "Please use a restart policy on docker run") - flags.StringVarP(&conf.GraphDriver, "storage-driver", "s", "", "Storage driver to use") + + // Windows doesn't support setting the storage driver - there is no choice as to which ones to use. + if runtime.GOOS != "windows" { + flags.StringVarP(&conf.GraphDriver, "storage-driver", "s", "", "Storage driver to use") + } + flags.IntVar(&conf.Mtu, "mtu", 0, "Set the containers network MTU") flags.BoolVar(&conf.RawLogs, "raw-logs", false, "Full timestamps without ANSI coloring") flags.Var(opts.NewListOptsRef(&conf.DNS, opts.ValidateIPAddress), "dns", "DNS server to use") diff --git a/cmd/dockerd/daemon.go b/cmd/dockerd/daemon.go index b8b4762340..ebd23611b9 100644 --- a/cmd/dockerd/daemon.go +++ b/cmd/dockerd/daemon.go @@ -40,6 +40,7 @@ import ( "github.com/docker/docker/pkg/pidfile" "github.com/docker/docker/pkg/plugingetter" "github.com/docker/docker/pkg/signal" + "github.com/docker/docker/pkg/system" "github.com/docker/docker/plugin" "github.com/docker/docker/registry" "github.com/docker/docker/runconfig" @@ -210,6 +211,10 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) { logrus.Fatalf("Error creating middlewares: %v", err) } + if system.LCOWSupported() { + logrus.Warnln("LCOW support is enabled - this feature is incomplete") + } + d, err := daemon.NewDaemon(cli.Config, registryService, containerdRemote, pluginStore) if err != nil { return fmt.Errorf("Error starting daemon: %v", err) @@ -262,12 +267,6 @@ func (cli *DaemonCli) start(opts *daemonOptions) (err error) { logrus.Info("Daemon has completed initialization") - logrus.WithFields(logrus.Fields{ - "version": dockerversion.Version, - "commit": dockerversion.GitCommit, - "graphdriver": d.GraphDriverName(), - }).Info("Docker daemon") - cli.d = d initRouter(api, d, c) diff --git a/container/container.go b/container/container.go index f22b4aa574..f1c8a10cd7 100644 --- a/container/container.go +++ b/container/container.go @@ -7,6 +7,7 @@ import ( "net" "os" "path/filepath" + "runtime" "strconv" "strings" "sync" @@ -33,6 +34,7 @@ import ( "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/signal" "github.com/docker/docker/pkg/symlink" + "github.com/docker/docker/pkg/system" "github.com/docker/docker/restartmanager" "github.com/docker/docker/runconfig" "github.com/docker/docker/volume" @@ -58,9 +60,8 @@ var ( errInvalidNetwork = fmt.Errorf("invalid network settings while building port map info") ) -// CommonContainer holds the fields for a container which are -// applicable across all platforms supported by the daemon. -type CommonContainer struct { +// Container holds the structure defining a container object. +type Container struct { StreamConfig *stream.Config // embed for Container to support states directly. *State `json:"State"` // Needed for Engine API version <= 1.11 @@ -78,6 +79,7 @@ type CommonContainer struct { LogPath string Name string Driver string + Platform string // MountLabel contains the options for the 'mount' command MountLabel string ProcessLabel string @@ -95,21 +97,31 @@ type CommonContainer struct { LogCopier *logger.Copier `json:"-"` restartManager restartmanager.RestartManager attachContext *attachContext + + // Fields here are specific to Unix platforms + AppArmorProfile string + HostnamePath string + HostsPath string + ShmPath string + ResolvConfPath string + SeccompProfile string + NoNewPrivileges bool + + // Fields here are specific to Windows + NetworkSharedContainerID string } // NewBaseContainer creates a new container with its // basic configuration. func NewBaseContainer(id, root string) *Container { return &Container{ - CommonContainer: CommonContainer{ - ID: id, - State: NewState(), - ExecCommands: exec.NewStore(), - Root: root, - MountPoints: make(map[string]*volume.MountPoint), - StreamConfig: stream.NewConfig(), - attachContext: &attachContext{}, - }, + ID: id, + State: NewState(), + ExecCommands: exec.NewStore(), + Root: root, + MountPoints: make(map[string]*volume.MountPoint), + StreamConfig: stream.NewConfig(), + attachContext: &attachContext{}, } } @@ -133,6 +145,13 @@ func (container *Container) FromDisk() error { return err } + // Ensure the platform is set if blank. Assume it is the platform of the + // host OS if not, to ensure containers created before multiple-platform + // support are migrated + if container.Platform == "" { + container.Platform = runtime.GOOS + } + if err := label.ReserveLabel(container.ProcessLabel); err != nil { return err } @@ -986,3 +1005,31 @@ func (container *Container) ConfigsDirPath() string { func (container *Container) ConfigFilePath(configRef swarmtypes.ConfigReference) string { return filepath.Join(container.ConfigsDirPath(), configRef.ConfigID) } + +// CreateDaemonEnvironment creates a new environment variable slice for this container. +func (container *Container) CreateDaemonEnvironment(tty bool, linkedEnv []string) []string { + // Setup environment + // TODO @jhowardmsft LCOW Support. This will need revisiting later. + platform := container.Platform + if platform == "" { + platform = runtime.GOOS + } + env := []string{} + if runtime.GOOS != "windows" || (runtime.GOOS == "windows" && system.LCOWSupported() && platform == "linux") { + env = []string{ + "PATH=" + system.DefaultPathEnv(platform), + "HOSTNAME=" + container.Config.Hostname, + } + if tty { + env = append(env, "TERM=xterm") + } + env = append(env, linkedEnv...) + } + + // because the env on the container can override certain default values + // we need to replace the 'env' keys where they match and append anything + // else. + //return ReplaceOrAppendEnvValues(linkedEnv, container.Config.Env) + foo := ReplaceOrAppendEnvValues(env, container.Config.Env) + return foo +} diff --git a/container/container_unit_test.go b/container/container_unit_test.go index 01d06e4eb8..9ba2991beb 100644 --- a/container/container_unit_test.go +++ b/container/container_unit_test.go @@ -11,9 +11,7 @@ import ( func TestContainerStopSignal(t *testing.T) { c := &Container{ - CommonContainer: CommonContainer{ - Config: &container.Config{}, - }, + Config: &container.Config{}, } def, err := signal.ParseSignal(signal.DefaultStopSignal) @@ -27,9 +25,7 @@ func TestContainerStopSignal(t *testing.T) { } c = &Container{ - CommonContainer: CommonContainer{ - Config: &container.Config{StopSignal: "SIGKILL"}, - }, + Config: &container.Config{StopSignal: "SIGKILL"}, } s = c.StopSignal() if s != 9 { @@ -39,9 +35,7 @@ func TestContainerStopSignal(t *testing.T) { func TestContainerStopTimeout(t *testing.T) { c := &Container{ - CommonContainer: CommonContainer{ - Config: &container.Config{}, - }, + Config: &container.Config{}, } s := c.StopTimeout() @@ -51,9 +45,7 @@ func TestContainerStopTimeout(t *testing.T) { stopTimeout := 15 c = &Container{ - CommonContainer: CommonContainer{ - Config: &container.Config{StopTimeout: &stopTimeout}, - }, + Config: &container.Config{StopTimeout: &stopTimeout}, } s = c.StopSignal() if s != 15 { diff --git a/container/container_unix.go b/container/container_unix.go index 4a26a94389..d53d057b52 100644 --- a/container/container_unix.go +++ b/container/container_unix.go @@ -26,21 +26,6 @@ const ( containerSecretMountPath = "/run/secrets" ) -// Container holds the fields specific to unixen implementations. -// See CommonContainer for standard fields common to all containers. -type Container struct { - CommonContainer - - // Fields below here are platform specific. - AppArmorProfile string - HostnamePath string - HostsPath string - ShmPath string - ResolvConfPath string - SeccompProfile string - NoNewPrivileges bool -} - // ExitStatus provides exit reasons for a container. type ExitStatus struct { // The exit code with which the container exited. @@ -50,27 +35,6 @@ type ExitStatus struct { OOMKilled bool } -// CreateDaemonEnvironment returns the list of all environment variables given the list of -// environment variables related to links. -// Sets PATH, HOSTNAME and if container.Config.Tty is set: TERM. -// The defaults set here do not override the values in container.Config.Env -func (container *Container) CreateDaemonEnvironment(tty bool, linkedEnv []string) []string { - // Setup environment - env := []string{ - "PATH=" + system.DefaultPathEnv, - "HOSTNAME=" + container.Config.Hostname, - } - if tty { - env = append(env, "TERM=xterm") - } - env = append(env, linkedEnv...) - // because the env on the container can override certain default values - // we need to replace the 'env' keys where they match and append anything - // else. - env = ReplaceOrAppendEnvValues(env, container.Config.Env) - return env -} - // TrySetNetworkMount attempts to set the network mounts given a provided destination and // the path to use for it; return true if the given destination was a network mount file func (container *Container) TrySetNetworkMount(destination string, path string) bool { diff --git a/container/container_windows.go b/container/container_windows.go index bffd9520e2..fe11140a29 100644 --- a/container/container_windows.go +++ b/container/container_windows.go @@ -17,29 +17,12 @@ const ( containerInternalConfigsDirPath = `C:\ProgramData\Docker\internal\configs` ) -// Container holds fields specific to the Windows implementation. See -// CommonContainer for standard fields common to all containers. -type Container struct { - CommonContainer - - // Fields below here are platform specific. - NetworkSharedContainerID string -} - // ExitStatus provides exit reasons for a container. type ExitStatus struct { // The exit code with which the container exited. ExitCode int } -// CreateDaemonEnvironment creates a new environment variable slice for this container. -func (container *Container) CreateDaemonEnvironment(_ bool, linkedEnv []string) []string { - // because the env on the container can override certain default values - // we need to replace the 'env' keys where they match and append anything - // else. - return ReplaceOrAppendEnvValues(linkedEnv, container.Config.Env) -} - // UnmountIpcMounts unmounts Ipc related mounts. // This is a NOOP on windows. func (container *Container) UnmountIpcMounts(unmount func(pth string) error) { @@ -60,7 +43,7 @@ func (container *Container) CreateSecretSymlinks() error { if err != nil { return err } - if err := system.MkdirAll(filepath.Dir(resolvedPath), 0); err != nil { + if err := system.MkdirAll(filepath.Dir(resolvedPath), 0, ""); err != nil { return err } if err := os.Symlink(filepath.Join(containerInternalSecretMountPath, r.SecretID), resolvedPath); err != nil { @@ -102,7 +85,7 @@ func (container *Container) CreateConfigSymlinks() error { if err != nil { return err } - if err := system.MkdirAll(filepath.Dir(resolvedPath), 0); err != nil { + if err := system.MkdirAll(filepath.Dir(resolvedPath), 0, ""); err != nil { return err } if err := os.Symlink(filepath.Join(containerInternalConfigsDirPath, configRef.ConfigID), resolvedPath); err != nil { diff --git a/daemon/build.go b/daemon/build.go index 93a48d51fe..9b518d64f3 100644 --- a/daemon/build.go +++ b/daemon/build.go @@ -2,6 +2,7 @@ package daemon import ( "io" + "runtime" "github.com/Sirupsen/logrus" "github.com/docker/distribution/reference" @@ -40,7 +41,7 @@ func (rl *releaseableLayer) Mount() (string, error) { return rl.rwLayer.Mount("") } -func (rl *releaseableLayer) Commit() (builder.ReleaseableLayer, error) { +func (rl *releaseableLayer) Commit(platform string) (builder.ReleaseableLayer, error) { var chainID layer.ChainID if rl.roLayer != nil { chainID = rl.roLayer.ChainID() @@ -51,7 +52,7 @@ func (rl *releaseableLayer) Commit() (builder.ReleaseableLayer, error) { return nil, err } - newLayer, err := rl.layerStore.Register(stream, chainID) + newLayer, err := rl.layerStore.Register(stream, chainID, layer.Platform(platform)) if err != nil { return nil, err } @@ -114,7 +115,7 @@ func newReleasableLayerForImage(img *image.Image, layerStore layer.Store) (build } // TODO: could this use the regular daemon PullImage ? -func (daemon *Daemon) pullForBuilder(ctx context.Context, name string, authConfigs map[string]types.AuthConfig, output io.Writer) (*image.Image, error) { +func (daemon *Daemon) pullForBuilder(ctx context.Context, name string, authConfigs map[string]types.AuthConfig, output io.Writer, platform string) (*image.Image, error) { ref, err := reference.ParseNormalizedNamed(name) if err != nil { return nil, err @@ -133,7 +134,7 @@ func (daemon *Daemon) pullForBuilder(ctx context.Context, name string, authConfi pullRegistryAuth = &resolvedConfig } - if err := daemon.pullImageWithReference(ctx, ref, nil, pullRegistryAuth, output); err != nil { + if err := daemon.pullImageWithReference(ctx, ref, platform, nil, pullRegistryAuth, output); err != nil { return nil, err } return daemon.GetImage(name) @@ -144,7 +145,7 @@ func (daemon *Daemon) pullForBuilder(ctx context.Context, name string, authConfi // leaking of layers. func (daemon *Daemon) GetImageAndReleasableLayer(ctx context.Context, refOrID string, opts backend.GetImageAndLayerOptions) (builder.Image, builder.ReleaseableLayer, error) { if refOrID == "" { - layer, err := newReleasableLayerForImage(nil, daemon.layerStore) + layer, err := newReleasableLayerForImage(nil, daemon.stores[opts.Platform].layerStore) return nil, layer, err } @@ -155,35 +156,38 @@ func (daemon *Daemon) GetImageAndReleasableLayer(ctx context.Context, refOrID st } // TODO: shouldn't we error out if error is different from "not found" ? if image != nil { - layer, err := newReleasableLayerForImage(image, daemon.layerStore) + layer, err := newReleasableLayerForImage(image, daemon.stores[opts.Platform].layerStore) return image, layer, err } } - image, err := daemon.pullForBuilder(ctx, refOrID, opts.AuthConfig, opts.Output) + image, err := daemon.pullForBuilder(ctx, refOrID, opts.AuthConfig, opts.Output, opts.Platform) if err != nil { return nil, nil, err } - layer, err := newReleasableLayerForImage(image, daemon.layerStore) + layer, err := newReleasableLayerForImage(image, daemon.stores[opts.Platform].layerStore) return image, layer, err } // CreateImage creates a new image by adding a config and ID to the image store. // This is similar to LoadImage() except that it receives JSON encoded bytes of // an image instead of a tar archive. -func (daemon *Daemon) CreateImage(config []byte, parent string) (builder.Image, error) { - id, err := daemon.imageStore.Create(config) +func (daemon *Daemon) CreateImage(config []byte, parent string, platform string) (builder.Image, error) { + if platform == "" { + platform = runtime.GOOS + } + id, err := daemon.stores[platform].imageStore.Create(config) if err != nil { return nil, errors.Wrapf(err, "failed to create image") } if parent != "" { - if err := daemon.imageStore.SetParent(id, image.ID(parent)); err != nil { + if err := daemon.stores[platform].imageStore.SetParent(id, image.ID(parent)); err != nil { return nil, errors.Wrapf(err, "failed to set parent %s", parent) } } - return daemon.imageStore.Get(id) + return daemon.stores[platform].imageStore.Get(id) } // IDMappings returns uid/gid mappings for the builder diff --git a/daemon/cache.go b/daemon/cache.go index 8e3d207758..219b0b38da 100644 --- a/daemon/cache.go +++ b/daemon/cache.go @@ -7,12 +7,12 @@ import ( ) // MakeImageCache creates a stateful image cache. -func (daemon *Daemon) MakeImageCache(sourceRefs []string) builder.ImageCache { +func (daemon *Daemon) MakeImageCache(sourceRefs []string, platform string) builder.ImageCache { if len(sourceRefs) == 0 { - return cache.NewLocal(daemon.imageStore) + return cache.NewLocal(daemon.stores[platform].imageStore) } - cache := cache.New(daemon.imageStore) + cache := cache.New(daemon.stores[platform].imageStore) for _, ref := range sourceRefs { img, err := daemon.GetImage(ref) diff --git a/daemon/cluster/executor/backend.go b/daemon/cluster/executor/backend.go index 2578a93c5a..fbe9006561 100644 --- a/daemon/cluster/executor/backend.go +++ b/daemon/cluster/executor/backend.go @@ -30,7 +30,7 @@ type Backend interface { FindNetwork(idName string) (libnetwork.Network, error) SetupIngress(clustertypes.NetworkCreateRequest, string) (<-chan struct{}, error) ReleaseIngress() (<-chan struct{}, error) - PullImage(ctx context.Context, image, tag string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error + PullImage(ctx context.Context, image, tag, platform string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error CreateManagedContainer(config types.ContainerCreateConfig) (container.ContainerCreateCreatedBody, error) ContainerStart(name string, hostConfig *container.HostConfig, checkpoint string, checkpointDir string) error ContainerStop(name string, seconds *int) error diff --git a/daemon/cluster/executor/container/adapter.go b/daemon/cluster/executor/container/adapter.go index 67d42c706e..75a085ef1d 100644 --- a/daemon/cluster/executor/container/adapter.go +++ b/daemon/cluster/executor/container/adapter.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "os" + "runtime" "strings" "syscall" "time" @@ -20,6 +21,7 @@ import ( containerpkg "github.com/docker/docker/container" "github.com/docker/docker/daemon/cluster/convert" executorpkg "github.com/docker/docker/daemon/cluster/executor" + "github.com/docker/docker/pkg/system" "github.com/docker/libnetwork" "github.com/docker/swarmkit/agent/exec" "github.com/docker/swarmkit/api" @@ -88,7 +90,13 @@ func (c *containerAdapter) pullImage(ctx context.Context) error { pr, pw := io.Pipe() metaHeaders := map[string][]string{} go func() { - err := c.backend.PullImage(ctx, c.container.image(), "", metaHeaders, authConfig, pw) + // TODO @jhowardmsft LCOW Support: This will need revisiting as + // the stack is built up to include LCOW support for swarm. + platform := runtime.GOOS + if platform == "windows" && system.LCOWSupported() { + platform = "linux" + } + err := c.backend.PullImage(ctx, c.container.image(), "", platform, metaHeaders, authConfig, pw) pw.CloseWithError(err) }() diff --git a/daemon/cluster/executor/container/health_test.go b/daemon/cluster/executor/container/health_test.go index 4abf0999b4..b6f188557f 100644 --- a/daemon/cluster/executor/container/health_test.go +++ b/daemon/cluster/executor/container/health_test.go @@ -38,14 +38,12 @@ func TestHealthStates(t *testing.T) { } c := &container.Container{ - CommonContainer: container.CommonContainer{ - ID: "id", - Name: "name", - Config: &containertypes.Config{ - Image: "image_name", - Labels: map[string]string{ - "com.docker.swarm.task.id": "id", - }, + ID: "id", + Name: "name", + Config: &containertypes.Config{ + Image: "image_name", + Labels: map[string]string{ + "com.docker.swarm.task.id": "id", }, }, } diff --git a/daemon/commit.go b/daemon/commit.go index 6011628092..084f488583 100644 --- a/daemon/commit.go +++ b/daemon/commit.go @@ -164,17 +164,17 @@ func (daemon *Daemon) Commit(name string, c *backend.ContainerCommitConfig) (str parent = new(image.Image) parent.RootFS = image.NewRootFS() } else { - parent, err = daemon.imageStore.Get(container.ImageID) + parent, err = daemon.stores[container.Platform].imageStore.Get(container.ImageID) if err != nil { return "", err } } - l, err := daemon.layerStore.Register(rwTar, parent.RootFS.ChainID()) + l, err := daemon.stores[container.Platform].layerStore.Register(rwTar, parent.RootFS.ChainID(), layer.Platform(container.Platform)) if err != nil { return "", err } - defer layer.ReleaseAndLog(daemon.layerStore, l) + defer layer.ReleaseAndLog(daemon.stores[container.Platform].layerStore, l) containerConfig := c.ContainerConfig if containerConfig == nil { @@ -188,18 +188,18 @@ func (daemon *Daemon) Commit(name string, c *backend.ContainerCommitConfig) (str Config: newConfig, DiffID: l.DiffID(), } - config, err := json.Marshal(image.NewChildImage(parent, cc)) + config, err := json.Marshal(image.NewChildImage(parent, cc, container.Platform)) if err != nil { return "", err } - id, err := daemon.imageStore.Create(config) + id, err := daemon.stores[container.Platform].imageStore.Create(config) if err != nil { return "", err } if container.ImageID != "" { - if err := daemon.imageStore.SetParent(id, container.ImageID); err != nil { + if err := daemon.stores[container.Platform].imageStore.SetParent(id, container.ImageID); err != nil { return "", err } } @@ -218,7 +218,7 @@ func (daemon *Daemon) Commit(name string, c *backend.ContainerCommitConfig) (str return "", err } } - if err := daemon.TagImageWithReference(id, newTag); err != nil { + if err := daemon.TagImageWithReference(id, container.Platform, newTag); err != nil { return "", err } imageRef = reference.FamiliarString(newTag) diff --git a/daemon/container.go b/daemon/container.go index 6a660ba4f3..aaa6d1ce75 100644 --- a/daemon/container.go +++ b/daemon/container.go @@ -111,7 +111,7 @@ func (daemon *Daemon) Register(c *container.Container) { daemon.idIndex.Add(c.ID) } -func (daemon *Daemon) newContainer(name string, config *containertypes.Config, hostConfig *containertypes.HostConfig, imgID image.ID, managed bool) (*container.Container, error) { +func (daemon *Daemon) newContainer(name string, platform string, config *containertypes.Config, hostConfig *containertypes.HostConfig, imgID image.ID, managed bool) (*container.Container, error) { var ( id string err error @@ -144,8 +144,8 @@ func (daemon *Daemon) newContainer(name string, config *containertypes.Config, h base.ImageID = imgID base.NetworkSettings = &network.Settings{IsAnonymousEndpoint: noExplicitName} base.Name = name - base.Driver = daemon.GraphDriverName() - + base.Driver = daemon.GraphDriverName(platform) + base.Platform = platform return base, err } diff --git a/daemon/container_operations_windows.go b/daemon/container_operations_windows.go index 058089e7ca..76fd7863aa 100644 --- a/daemon/container_operations_windows.go +++ b/daemon/container_operations_windows.go @@ -25,7 +25,7 @@ func (daemon *Daemon) setupConfigDir(c *container.Container) (setupErr error) { logrus.Debugf("configs: setting up config dir: %s", localPath) // create local config root - if err := system.MkdirAllWithACL(localPath, 0); err != nil { + if err := system.MkdirAllWithACL(localPath, 0, system.SddlAdministratorsLocalSystem); err != nil { return errors.Wrap(err, "error creating config dir") } @@ -98,7 +98,7 @@ func (daemon *Daemon) setupSecretDir(c *container.Container) (setupErr error) { logrus.Debugf("secrets: setting up secret dir: %s", localMountPath) // create local secret root - if err := system.MkdirAllWithACL(localMountPath, 0); err != nil { + if err := system.MkdirAllWithACL(localMountPath, 0, system.SddlAdministratorsLocalSystem); err != nil { return errors.Wrap(err, "error creating secret local directory") } diff --git a/daemon/create.go b/daemon/create.go index 697886274f..addc9b718a 100644 --- a/daemon/create.go +++ b/daemon/create.go @@ -19,6 +19,7 @@ import ( "github.com/docker/docker/layer" "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/system" "github.com/docker/docker/runconfig" "github.com/opencontainers/selinux/go-selinux/label" ) @@ -75,6 +76,16 @@ func (daemon *Daemon) create(params types.ContainerCreateConfig, managed bool) ( err error ) + // TODO: @jhowardmsft LCOW support - at a later point, can remove the hard-coding + // to force the platform to be linux. + // Default the platform if not supplied + if params.Platform == "" { + params.Platform = runtime.GOOS + } + if params.Platform == "windows" && system.LCOWSupported() { + params.Platform = "linux" + } + if params.Config.Image != "" { img, err = daemon.GetImage(params.Config.Image) if err != nil { @@ -82,9 +93,23 @@ func (daemon *Daemon) create(params types.ContainerCreateConfig, managed bool) ( } if runtime.GOOS == "solaris" && img.OS != "solaris " { - return nil, errors.New("Platform on which parent image was created is not Solaris") + return nil, errors.New("platform on which parent image was created is not Solaris") } imgID = img.ID() + + if runtime.GOOS == "windows" && img.OS == "linux" && !system.LCOWSupported() { + return nil, errors.New("platform on which parent image was created is not Windows") + } + } + + // Make sure the platform requested matches the image + if img != nil { + if params.Platform != img.Platform() { + // Ignore this in LCOW mode. @jhowardmsft TODO - This will need revisiting later. + if !(runtime.GOOS == "windows" && system.LCOWSupported()) { + return nil, fmt.Errorf("cannot create a %s container from a %s image", params.Platform, img.Platform()) + } + } } if err := daemon.mergeAndVerifyConfig(params.Config, img); err != nil { @@ -95,7 +120,7 @@ func (daemon *Daemon) create(params types.ContainerCreateConfig, managed bool) ( return nil, err } - if container, err = daemon.newContainer(params.Name, params.Config, params.HostConfig, imgID, managed); err != nil { + if container, err = daemon.newContainer(params.Name, params.Platform, params.Config, params.HostConfig, imgID, managed); err != nil { return nil, err } defer func() { @@ -215,7 +240,7 @@ func (daemon *Daemon) generateSecurityOpt(hostConfig *containertypes.HostConfig) func (daemon *Daemon) setRWLayer(container *container.Container) error { var layerID layer.ChainID if container.ImageID != "" { - img, err := daemon.imageStore.Get(container.ImageID) + img, err := daemon.stores[container.Platform].imageStore.Get(container.ImageID) if err != nil { return err } @@ -228,7 +253,7 @@ func (daemon *Daemon) setRWLayer(container *container.Container) error { StorageOpt: container.HostConfig.StorageOpt, } - rwLayer, err := daemon.layerStore.CreateRWLayer(container.ID, layerID, rwLayerOpts) + rwLayer, err := daemon.stores[container.Platform].layerStore.CreateRWLayer(container.ID, layerID, rwLayerOpts) if err != nil { return err } diff --git a/daemon/daemon.go b/daemon/daemon.go index e7588b0b63..56474a49f0 100644 --- a/daemon/daemon.go +++ b/daemon/daemon.go @@ -69,43 +69,49 @@ var ( errSystemNotSupported = errors.New("The Docker daemon is not supported on this platform.") ) +type daemonStore struct { + graphDriver string + imageRoot string + imageStore image.Store + layerStore layer.Store + distributionMetadataStore dmetadata.Store + referenceStore refstore.Store +} + // Daemon holds information about the Docker daemon. type Daemon struct { - ID string - repository string - containers container.Store - execCommands *exec.Store - referenceStore refstore.Store - downloadManager *xfer.LayerDownloadManager - uploadManager *xfer.LayerUploadManager - distributionMetadataStore dmetadata.Store - trustKey libtrust.PrivateKey - idIndex *truncindex.TruncIndex - configStore *config.Config - statsCollector *stats.Collector - defaultLogConfig containertypes.LogConfig - RegistryService registry.Service - EventsService *events.Events - netController libnetwork.NetworkController - volumes *store.VolumeStore - discoveryWatcher discovery.Reloader - root string - seccompEnabled bool - apparmorEnabled bool - shutdown bool - idMappings *idtools.IDMappings - layerStore layer.Store - imageStore image.Store - PluginStore *plugin.Store // todo: remove - pluginManager *plugin.Manager - nameIndex *registrar.Registrar - linkIndex *linkIndex - containerd libcontainerd.Client - containerdRemote libcontainerd.Remote - defaultIsolation containertypes.Isolation // Default isolation mode on Windows - clusterProvider cluster.Provider - cluster Cluster - metricsPluginListener net.Listener + ID string + repository string + containers container.Store + execCommands *exec.Store + downloadManager *xfer.LayerDownloadManager + uploadManager *xfer.LayerUploadManager + trustKey libtrust.PrivateKey + idIndex *truncindex.TruncIndex + configStore *config.Config + statsCollector *stats.Collector + defaultLogConfig containertypes.LogConfig + RegistryService registry.Service + EventsService *events.Events + netController libnetwork.NetworkController + volumes *store.VolumeStore + discoveryWatcher discovery.Reloader + root string + seccompEnabled bool + apparmorEnabled bool + shutdown bool + idMappings *idtools.IDMappings + stores map[string]daemonStore // By container target platform + PluginStore *plugin.Store // todo: remove + pluginManager *plugin.Manager + nameIndex *registrar.Registrar + linkIndex *linkIndex + containerd libcontainerd.Client + containerdRemote libcontainerd.Remote + defaultIsolation containertypes.Isolation // Default isolation mode on Windows + clusterProvider cluster.Provider + cluster Cluster + metricsPluginListener net.Listener machineMemory uint64 @@ -137,10 +143,7 @@ func (daemon *Daemon) HasExperimental() bool { } func (daemon *Daemon) restore() error { - var ( - currentDriver = daemon.GraphDriverName() - containers = make(map[string]*container.Container) - ) + containers := make(map[string]*container.Container) logrus.Info("Loading containers: start.") @@ -158,8 +161,9 @@ func (daemon *Daemon) restore() error { } // Ignore the container if it does not support the current driver being used by the graph - if (container.Driver == "" && currentDriver == "aufs") || container.Driver == currentDriver { - rwlayer, err := daemon.layerStore.GetRWLayer(container.ID) + currentDriverForContainerPlatform := daemon.stores[container.Platform].graphDriver + if (container.Driver == "" && currentDriverForContainerPlatform == "aufs") || container.Driver == currentDriverForContainerPlatform { + rwlayer, err := daemon.stores[container.Platform].layerStore.GetRWLayer(container.ID) if err != nil { logrus.Errorf("Failed to load container mount %v: %v", id, err) continue @@ -590,14 +594,29 @@ func NewDaemon(config *config.Config, registryService registry.Service, containe } if runtime.GOOS == "windows" { - if err := system.MkdirAll(filepath.Join(config.Root, "credentialspecs"), 0); err != nil && !os.IsExist(err) { + if err := system.MkdirAll(filepath.Join(config.Root, "credentialspecs"), 0, ""); err != nil && !os.IsExist(err) { return nil, err } } - driverName := os.Getenv("DOCKER_DRIVER") - if driverName == "" { - driverName = config.GraphDriver + // On Windows we don't support the environment variable, or a user supplied graphdriver + // as Windows has no choice in terms of which graphdrivers to use. It's a case of + // running Windows containers on Windows - windowsfilter, running Linux containers on Windows, + // lcow. Unix platforms however run a single graphdriver for all containers, and it can + // be set through an environment variable, a daemon start parameter, or chosen through + // initialization of the layerstore through driver priority order for example. + d.stores = make(map[string]daemonStore) + if runtime.GOOS == "windows" { + d.stores["windows"] = daemonStore{graphDriver: "windowsfilter"} + if system.LCOWSupported() { + d.stores["linux"] = daemonStore{graphDriver: "lcow"} + } + } else { + driverName := os.Getenv("DOCKER_DRIVER") + if driverName == "" { + driverName = config.GraphDriver + } + d.stores[runtime.GOOS] = daemonStore{graphDriver: driverName} // May still be empty. Layerstore init determines instead. } d.RegistryService = registryService @@ -625,40 +644,56 @@ func NewDaemon(config *config.Config, registryService registry.Service, containe return nil, errors.Wrap(err, "couldn't create plugin manager") } - d.layerStore, err = layer.NewStoreFromOptions(layer.StoreOptions{ - StorePath: config.Root, - MetadataStorePathTemplate: filepath.Join(config.Root, "image", "%s", "layerdb"), - GraphDriver: driverName, - GraphDriverOptions: config.GraphOptions, - IDMappings: idMappings, - PluginGetter: d.PluginStore, - ExperimentalEnabled: config.Experimental, - }) - if err != nil { - return nil, err + var graphDrivers []string + for platform, ds := range d.stores { + ls, err := layer.NewStoreFromOptions(layer.StoreOptions{ + StorePath: config.Root, + MetadataStorePathTemplate: filepath.Join(config.Root, "image", "%s", "layerdb"), + GraphDriver: ds.graphDriver, + GraphDriverOptions: config.GraphOptions, + IDMappings: idMappings, + PluginGetter: d.PluginStore, + ExperimentalEnabled: config.Experimental, + Platform: platform, + }) + if err != nil { + return nil, err + } + ds.graphDriver = ls.DriverName() // As layerstore may set the driver + ds.layerStore = ls + d.stores[platform] = ds + graphDrivers = append(graphDrivers, ls.DriverName()) } - graphDriver := d.layerStore.DriverName() - imageRoot := filepath.Join(config.Root, "image", graphDriver) - // Configure and validate the kernels security support - if err := configureKernelSecuritySupport(config, graphDriver); err != nil { + if err := configureKernelSecuritySupport(config, graphDrivers); err != nil { return nil, err } logrus.Debugf("Max Concurrent Downloads: %d", *config.MaxConcurrentDownloads) - d.downloadManager = xfer.NewLayerDownloadManager(d.layerStore, *config.MaxConcurrentDownloads) + lsMap := make(map[string]layer.Store) + for platform, ds := range d.stores { + lsMap[platform] = ds.layerStore + } + d.downloadManager = xfer.NewLayerDownloadManager(lsMap, *config.MaxConcurrentDownloads) logrus.Debugf("Max Concurrent Uploads: %d", *config.MaxConcurrentUploads) d.uploadManager = xfer.NewLayerUploadManager(*config.MaxConcurrentUploads) - ifs, err := image.NewFSStoreBackend(filepath.Join(imageRoot, "imagedb")) - if err != nil { - return nil, err - } + for platform, ds := range d.stores { + imageRoot := filepath.Join(config.Root, "image", ds.graphDriver) + ifs, err := image.NewFSStoreBackend(filepath.Join(imageRoot, "imagedb")) + if err != nil { + return nil, err + } - d.imageStore, err = image.NewImageStore(ifs, d.layerStore) - if err != nil { - return nil, err + var is image.Store + is, err = image.NewImageStore(ifs, platform, ds.layerStore) + if err != nil { + return nil, err + } + ds.imageRoot = imageRoot + ds.imageStore = is + d.stores[platform] = ds } // Configure the volumes driver @@ -674,27 +709,35 @@ func NewDaemon(config *config.Config, registryService registry.Service, containe trustDir := filepath.Join(config.Root, "trust") - if err := system.MkdirAll(trustDir, 0700); err != nil { - return nil, err - } - - distributionMetadataStore, err := dmetadata.NewFSMetadataStore(filepath.Join(imageRoot, "distribution")) - if err != nil { + if err := system.MkdirAll(trustDir, 0700, ""); err != nil { return nil, err } eventsService := events.New() - referenceStore, err := refstore.NewReferenceStore(filepath.Join(imageRoot, "repositories.json")) - if err != nil { - return nil, fmt.Errorf("Couldn't create Tag store repositories: %s", err) - } + for platform, ds := range d.stores { + dms, err := dmetadata.NewFSMetadataStore(filepath.Join(ds.imageRoot, "distribution"), platform) + if err != nil { + return nil, err + } - migrationStart := time.Now() - if err := v1.Migrate(config.Root, graphDriver, d.layerStore, d.imageStore, referenceStore, distributionMetadataStore); err != nil { - logrus.Errorf("Graph migration failed: %q. Your old graph data was found to be too inconsistent for upgrading to content-addressable storage. Some of the old data was probably not upgraded. We recommend starting over with a clean storage directory if possible.", err) + rs, err := refstore.NewReferenceStore(filepath.Join(ds.imageRoot, "repositories.json"), platform) + if err != nil { + return nil, fmt.Errorf("Couldn't create Tag store repositories: %s", err) + } + ds.distributionMetadataStore = dms + ds.referenceStore = rs + d.stores[platform] = ds + + // No content-addressability migration on Windows as it never supported pre-CA + if runtime.GOOS != "windows" { + migrationStart := time.Now() + if err := v1.Migrate(config.Root, ds.graphDriver, ds.layerStore, ds.imageStore, rs, dms); err != nil { + logrus.Errorf("Graph migration failed: %q. Your old graph data was found to be too inconsistent for upgrading to content-addressable storage. Some of the old data was probably not upgraded. We recommend starting over with a clean storage directory if possible.", err) + } + logrus.Infof("Graph migration to content-addressability took %.2f seconds", time.Since(migrationStart).Seconds()) + } } - logrus.Infof("Graph migration to content-addressability took %.2f seconds", time.Since(migrationStart).Seconds()) // Discovery is only enabled when the daemon is launched with an address to advertise. When // initialized, the daemon is registered and we can store the discovery backend as it's read-only @@ -713,8 +756,6 @@ func NewDaemon(config *config.Config, registryService registry.Service, containe d.repository = daemonRepo d.containers = container.NewMemoryStore() d.execCommands = exec.NewStore() - d.referenceStore = referenceStore - d.distributionMetadataStore = distributionMetadataStore d.trustKey = trustKey d.idIndex = truncindex.NewTruncIndex([]string{}) d.statsCollector = d.newStatsCollector(1 * time.Second) @@ -761,6 +802,22 @@ func NewDaemon(config *config.Config, registryService registry.Service, containe engineCpus.Set(float64(info.NCPU)) engineMemory.Set(float64(info.MemTotal)) + gd := "" + for platform, ds := range d.stores { + if len(gd) > 0 { + gd += ", " + } + gd += ds.graphDriver + if len(d.stores) > 1 { + gd = fmt.Sprintf("%s (%s)", gd, platform) + } + } + logrus.WithFields(logrus.Fields{ + "version": dockerversion.Version, + "commit": dockerversion.GitCommit, + "graphdriver(s)": gd, + }).Info("Docker daemon") + return d, nil } @@ -867,7 +924,7 @@ func (daemon *Daemon) Shutdown() error { logrus.Errorf("Stop container error: %v", err) return } - if mountid, err := daemon.layerStore.GetMountID(c.ID); err == nil { + if mountid, err := daemon.stores[c.Platform].layerStore.GetMountID(c.ID); err == nil { daemon.cleanupMountsByID(mountid) } logrus.Debugf("container stopped %s", c.ID) @@ -880,9 +937,11 @@ func (daemon *Daemon) Shutdown() error { } } - if daemon.layerStore != nil { - if err := daemon.layerStore.Cleanup(); err != nil { - logrus.Errorf("Error during layer Store.Cleanup(): %v", err) + for platform, ds := range daemon.stores { + if ds.layerStore != nil { + if err := ds.layerStore.Cleanup(); err != nil { + logrus.Errorf("Error during layer Store.Cleanup(): %v %s", err, platform) + } } } @@ -925,7 +984,7 @@ func (daemon *Daemon) Mount(container *container.Container) error { if container.BaseFS != "" && runtime.GOOS != "windows" { daemon.Unmount(container) return fmt.Errorf("Error: driver %s is returning inconsistent paths for container %s ('%s' then '%s')", - daemon.GraphDriverName(), container.ID, container.BaseFS, dir) + daemon.GraphDriverName(container.Platform), container.ID, container.BaseFS, dir) } } container.BaseFS = dir // TODO: combine these fields @@ -967,8 +1026,8 @@ func (daemon *Daemon) Subnets() ([]net.IPNet, []net.IPNet) { } // GraphDriverName returns the name of the graph driver used by the layer.Store -func (daemon *Daemon) GraphDriverName() string { - return daemon.layerStore.DriverName() +func (daemon *Daemon) GraphDriverName(platform string) string { + return daemon.stores[platform].layerStore.DriverName() } // prepareTempDir prepares and returns the default directory to use diff --git a/daemon/daemon_solaris.go b/daemon/daemon_solaris.go index de0f3ac3e7..7f2004e65a 100644 --- a/daemon/daemon_solaris.go +++ b/daemon/daemon_solaris.go @@ -353,7 +353,7 @@ func configureMaxThreads(config *Config) error { } // configureKernelSecuritySupport configures and validate security support for the kernel -func configureKernelSecuritySupport(config *Config, driverName string) error { +func configureKernelSecuritySupport(config *config.Config, driverNames []string) error { return nil } diff --git a/daemon/daemon_test.go b/daemon/daemon_test.go index e728abddf8..6f07d0d1ee 100644 --- a/daemon/daemon_test.go +++ b/daemon/daemon_test.go @@ -27,38 +27,28 @@ import ( func TestGetContainer(t *testing.T) { c1 := &container.Container{ - CommonContainer: container.CommonContainer{ - ID: "5a4ff6a163ad4533d22d69a2b8960bf7fafdcba06e72d2febdba229008b0bf57", - Name: "tender_bardeen", - }, + ID: "5a4ff6a163ad4533d22d69a2b8960bf7fafdcba06e72d2febdba229008b0bf57", + Name: "tender_bardeen", } c2 := &container.Container{ - CommonContainer: container.CommonContainer{ - ID: "3cdbd1aa394fd68559fd1441d6eff2ab7c1e6363582c82febfaa8045df3bd8de", - Name: "drunk_hawking", - }, + ID: "3cdbd1aa394fd68559fd1441d6eff2ab7c1e6363582c82febfaa8045df3bd8de", + Name: "drunk_hawking", } c3 := &container.Container{ - CommonContainer: container.CommonContainer{ - ID: "3cdbd1aa394fd68559fd1441d6eff2abfafdcba06e72d2febdba229008b0bf57", - Name: "3cdbd1aa", - }, + ID: "3cdbd1aa394fd68559fd1441d6eff2abfafdcba06e72d2febdba229008b0bf57", + Name: "3cdbd1aa", } c4 := &container.Container{ - CommonContainer: container.CommonContainer{ - ID: "75fb0b800922abdbef2d27e60abcdfaf7fb0698b2a96d22d3354da361a6ff4a5", - Name: "5a4ff6a163ad4533d22d69a2b8960bf7fafdcba06e72d2febdba229008b0bf57", - }, + ID: "75fb0b800922abdbef2d27e60abcdfaf7fb0698b2a96d22d3354da361a6ff4a5", + Name: "5a4ff6a163ad4533d22d69a2b8960bf7fafdcba06e72d2febdba229008b0bf57", } c5 := &container.Container{ - CommonContainer: container.CommonContainer{ - ID: "d22d69a2b8960bf7fafdcba06e72d2febdba960bf7fafdcba06e72d2f9008b060b", - Name: "d22d69a2b896", - }, + ID: "d22d69a2b8960bf7fafdcba06e72d2febdba960bf7fafdcba06e72d2f9008b060b", + Name: "d22d69a2b896", } store := container.NewMemoryStore() @@ -184,7 +174,7 @@ func TestContainerInitDNS(t *testing.T) { "UpdateDns":false,"Volumes":{},"VolumesRW":{},"AppliedVolumesFrom":null}` // Container struct only used to retrieve path to config file - container := &container.Container{CommonContainer: container.CommonContainer{Root: containerPath}} + container := &container.Container{Root: containerPath} configPath, err := container.ConfigPath() if err != nil { t.Fatal(err) diff --git a/daemon/daemon_unix.go b/daemon/daemon_unix.go index 07c323d63f..662098ed19 100644 --- a/daemon/daemon_unix.go +++ b/daemon/daemon_unix.go @@ -702,14 +702,22 @@ func overlaySupportsSelinux() (bool, error) { } // configureKernelSecuritySupport configures and validates security support for the kernel -func configureKernelSecuritySupport(config *config.Config, driverName string) error { +func configureKernelSecuritySupport(config *config.Config, driverNames []string) error { if config.EnableSelinuxSupport { if !selinuxEnabled() { logrus.Warn("Docker could not enable SELinux on the host system") return nil } - if driverName == "overlay" || driverName == "overlay2" { + overlayFound := false + for _, d := range driverNames { + if d == "overlay" || d == "overlay2" { + overlayFound = true + break + } + } + + if overlayFound { // If driver is overlay or overlay2, make sure kernel // supports selinux with overlay. supported, err := overlaySupportsSelinux() @@ -718,7 +726,7 @@ func configureKernelSecuritySupport(config *config.Config, driverName string) er } if !supported { - logrus.Warnf("SELinux is not supported with the %s graph driver on this kernel", driverName) + logrus.Warnf("SELinux is not supported with the %v graph driver on this kernel", driverNames) } } } else { diff --git a/daemon/daemon_windows.go b/daemon/daemon_windows.go index 49a7b212b8..c77f32a5ef 100644 --- a/daemon/daemon_windows.go +++ b/daemon/daemon_windows.go @@ -260,7 +260,7 @@ func checkSystem() error { } // configureKernelSecuritySupport configures and validate security support for the kernel -func configureKernelSecuritySupport(config *config.Config, driverName string) error { +func configureKernelSecuritySupport(config *config.Config, driverNames []string) error { return nil } @@ -465,7 +465,7 @@ func setupRemappedRoot(config *config.Config) (*idtools.IDMappings, error) { func setupDaemonRoot(config *config.Config, rootDir string, rootIDs idtools.IDPair) error { config.Root = rootDir // Create the root directory if it doesn't exists - if err := system.MkdirAllWithACL(config.Root, 0); err != nil && !os.IsExist(err) { + if err := system.MkdirAllWithACL(config.Root, 0, system.SddlAdministratorsLocalSystem); err != nil && !os.IsExist(err) { return err } return nil @@ -486,6 +486,11 @@ func (daemon *Daemon) runAsHyperVContainer(hostConfig *containertypes.HostConfig // conditionalMountOnStart is a platform specific helper function during the // container start to call mount. func (daemon *Daemon) conditionalMountOnStart(container *container.Container) error { + // Bail out now for Linux containers + if system.LCOWSupported() && container.Platform != "windows" { + return nil + } + // We do not mount if a Hyper-V container if !daemon.runAsHyperVContainer(container.HostConfig) { return daemon.Mount(container) @@ -496,6 +501,11 @@ func (daemon *Daemon) conditionalMountOnStart(container *container.Container) er // conditionalUnmountOnCleanup is a platform specific helper function called // during the cleanup of a container to unmount. func (daemon *Daemon) conditionalUnmountOnCleanup(container *container.Container) error { + // Bail out now for Linux containers + if system.LCOWSupported() && container.Platform != "windows" { + return nil + } + // We do not unmount if a Hyper-V container if !daemon.runAsHyperVContainer(container.HostConfig) { return daemon.Unmount(container) diff --git a/daemon/delete.go b/daemon/delete.go index af709445ce..3dcd289101 100644 --- a/daemon/delete.go +++ b/daemon/delete.go @@ -115,10 +115,10 @@ func (daemon *Daemon) cleanupContainer(container *container.Container, forceRemo // When container creation fails and `RWLayer` has not been created yet, we // do not call `ReleaseRWLayer` if container.RWLayer != nil { - metadata, err := daemon.layerStore.ReleaseRWLayer(container.RWLayer) + metadata, err := daemon.stores[container.Platform].layerStore.ReleaseRWLayer(container.RWLayer) layer.LogReleaseMetadata(metadata) if err != nil && err != layer.ErrMountDoesNotExist { - return errors.Wrapf(err, "driver %q failed to remove root filesystem for %s", daemon.GraphDriverName(), container.ID) + return errors.Wrapf(err, "driver %q failed to remove root filesystem for %s", daemon.GraphDriverName(container.Platform), container.ID) } } diff --git a/daemon/delete_test.go b/daemon/delete_test.go index 4664cd9869..f1a979003c 100644 --- a/daemon/delete_test.go +++ b/daemon/delete_test.go @@ -26,11 +26,9 @@ func newDaemonWithTmpRoot(t *testing.T) (*Daemon, func()) { func newContainerWithState(state *container.State) *container.Container { return &container.Container{ - CommonContainer: container.CommonContainer{ - ID: "test", - State: state, - Config: &containertypes.Config{}, - }, + ID: "test", + State: state, + Config: &containertypes.Config{}, } } diff --git a/daemon/disk_usage.go b/daemon/disk_usage.go index fb15c3c373..c64a243304 100644 --- a/daemon/disk_usage.go +++ b/daemon/disk_usage.go @@ -15,12 +15,12 @@ import ( "github.com/opencontainers/go-digest" ) -func (daemon *Daemon) getLayerRefs() map[layer.ChainID]int { - tmpImages := daemon.imageStore.Map() +func (daemon *Daemon) getLayerRefs(platform string) map[layer.ChainID]int { + tmpImages := daemon.stores[platform].imageStore.Map() layerRefs := map[layer.ChainID]int{} for id, img := range tmpImages { dgst := digest.Digest(id) - if len(daemon.referenceStore.References(dgst)) == 0 && len(daemon.imageStore.Children(id)) != 0 { + if len(daemon.stores[platform].referenceStore.References(dgst)) == 0 && len(daemon.stores[platform].imageStore.Children(id)) != 0 { continue } @@ -53,6 +53,7 @@ func (daemon *Daemon) SystemDiskUsage(ctx context.Context) (*types.DiskUsage, er } // Get all top images with extra attributes + // TODO @jhowardmsft LCOW. This may need revisiting allImages, err := daemon.Images(filters.NewArgs(), false, true) if err != nil { return nil, fmt.Errorf("failed to retrieve image list: %v", err) @@ -94,23 +95,26 @@ func (daemon *Daemon) SystemDiskUsage(ctx context.Context) (*types.DiskUsage, er } // Get total layers size on disk - layerRefs := daemon.getLayerRefs() - allLayers := daemon.layerStore.Map() var allLayersSize int64 - for _, l := range allLayers { - select { - case <-ctx.Done(): - return nil, ctx.Err() - default: - size, err := l.DiffSize() - if err == nil { - if _, ok := layerRefs[l.ChainID()]; ok { - allLayersSize += size + for platform := range daemon.stores { + layerRefs := daemon.getLayerRefs(platform) + allLayers := daemon.stores[platform].layerStore.Map() + var allLayersSize int64 + for _, l := range allLayers { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + size, err := l.DiffSize() + if err == nil { + if _, ok := layerRefs[l.ChainID()]; ok { + allLayersSize += size + } else { + logrus.Warnf("found leaked image layer %v platform %s", l.ChainID(), platform) + } } else { - logrus.Warnf("found leaked image layer %v", l.ChainID()) + logrus.Warnf("failed to get diff size for layer %v %s", l.ChainID(), platform) } - } else { - logrus.Warnf("failed to get diff size for layer %v", l.ChainID()) } } } diff --git a/daemon/events_test.go b/daemon/events_test.go index aa78664b23..7048de2926 100644 --- a/daemon/events_test.go +++ b/daemon/events_test.go @@ -16,15 +16,13 @@ func TestLogContainerEventCopyLabels(t *testing.T) { defer e.Evict(l) container := &container.Container{ - CommonContainer: container.CommonContainer{ - ID: "container_id", - Name: "container_name", - Config: &containertypes.Config{ - Image: "image_name", - Labels: map[string]string{ - "node": "1", - "os": "alpine", - }, + ID: "container_id", + Name: "container_name", + Config: &containertypes.Config{ + Image: "image_name", + Labels: map[string]string{ + "node": "1", + "os": "alpine", }, }, } @@ -49,14 +47,12 @@ func TestLogContainerEventWithAttributes(t *testing.T) { defer e.Evict(l) container := &container.Container{ - CommonContainer: container.CommonContainer{ - ID: "container_id", - Name: "container_name", - Config: &containertypes.Config{ - Labels: map[string]string{ - "node": "1", - "os": "alpine", - }, + ID: "container_id", + Name: "container_name", + Config: &containertypes.Config{ + Labels: map[string]string{ + "node": "1", + "os": "alpine", }, }, } diff --git a/daemon/getsize_unix.go b/daemon/getsize_unix.go index bd088fd41c..434fa4388a 100644 --- a/daemon/getsize_unix.go +++ b/daemon/getsize_unix.go @@ -3,6 +3,8 @@ package daemon import ( + "runtime" + "github.com/Sirupsen/logrus" ) @@ -13,17 +15,17 @@ func (daemon *Daemon) getSize(containerID string) (int64, int64) { err error ) - rwlayer, err := daemon.layerStore.GetRWLayer(containerID) + rwlayer, err := daemon.stores[runtime.GOOS].layerStore.GetRWLayer(containerID) if err != nil { logrus.Errorf("Failed to compute size of container rootfs %v: %v", containerID, err) return sizeRw, sizeRootfs } - defer daemon.layerStore.ReleaseRWLayer(rwlayer) + defer daemon.stores[runtime.GOOS].layerStore.ReleaseRWLayer(rwlayer) sizeRw, err = rwlayer.Size() if err != nil { logrus.Errorf("Driver %s couldn't return diff size of container %s: %s", - daemon.GraphDriverName(), containerID, err) + daemon.GraphDriverName(runtime.GOOS), containerID, err) // FIXME: GetSize should return an error. Not changing it now in case // there is a side-effect. sizeRw = -1 diff --git a/daemon/graphdriver/lcow/lcow.go b/daemon/graphdriver/lcow/lcow.go new file mode 100644 index 0000000000..6bf491574a --- /dev/null +++ b/daemon/graphdriver/lcow/lcow.go @@ -0,0 +1,497 @@ +// +build windows + +package lcow + +// Maintainer: @jhowardmsft +// Graph-driver for Linux Containers On Windows (LCOW) + +import ( + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/Sirupsen/logrus" + "github.com/docker/docker/daemon/graphdriver" + "github.com/docker/docker/pkg/archive" + "github.com/docker/docker/pkg/idtools" + "github.com/docker/docker/pkg/system" + "github.com/jhowardmsft/opengcs/gogcs/client" +) + +// init registers the LCOW driver to the register. +func init() { + graphdriver.Register("lcow", InitLCOW) +} + +const ( + // sandboxFilename is the name of the file containing a layers sandbox (read-write layer) + sandboxFilename = "sandbox.vhdx" +) + +// cacheType is our internal structure representing an item in our local cache +// of things that have been mounted. +type cacheType struct { + uvmPath string // Path in utility VM + hostPath string // Path on host + refCount int // How many times its been mounted + isSandbox bool // True if a sandbox +} + +// Driver represents an LCOW graph driver. +type Driver struct { + // homeDir is the hostpath where we're storing everything + homeDir string + // cachedSandboxFile is the location of the local default-sized cached sandbox + cachedSandboxFile string + // options are the graphdriver options we are initialised with + options []string + // JJH LIFETIME TODO - Remove this and move up to daemon. For now, a global service utility-VM + config client.Config + + // it is safe for windows to use a cache here because it does not support + // restoring containers when the daemon dies. + + // cacheMu is the mutex protection add/update/deletes to our cache + cacheMu sync.Mutex + // cache is the cache of all the IDs we've mounted/unmounted. + cache map[string]cacheType +} + +// InitLCOW returns a new LCOW storage driver. +func InitLCOW(home string, options []string, uidMaps, gidMaps []idtools.IDMap) (graphdriver.Driver, error) { + title := "lcowdriver: init:" + logrus.Debugf("%s %s", title, home) + + d := &Driver{ + homeDir: home, + options: options, + cachedSandboxFile: filepath.Join(home, "cache", sandboxFilename), + cache: make(map[string]cacheType), + } + + if err := idtools.MkdirAllAs(home, 0700, 0, 0); err != nil { + return nil, fmt.Errorf("%s failed to create '%s': %v", title, home, err) + } + + // Cache directory for blank sandbox so don't have to pull it from the service VM each time + if err := idtools.MkdirAllAs(filepath.Dir(d.cachedSandboxFile), 0700, 0, 0); err != nil { + return nil, fmt.Errorf("%s failed to create '%s': %v", title, home, err) + } + + return d, nil +} + +// startUvm starts the service utility VM if it isn't running. +// TODO @jhowardmsft. This will change before RS3 ships as we move to a model of one +// service VM globally to a service VM per container (or offline operation). However, +// for the initial bring-up of LCOW, this is acceptable. +func (d *Driver) startUvm(context string) error { + // Nothing to do if it's already running + if d.config.Uvm != nil { + return nil + } + + // So we need to start it. Generate a default configuration + if err := d.config.GenerateDefault(d.options); err != nil { + return fmt.Errorf("failed to generate default gogcs configuration (%s): %s", context, err) + } + + d.config.Name = "LinuxServiceVM" // TODO @jhowardmsft - This requires an in-flight platform change. Can't hard code it to this longer term + if err := d.config.Create(); err != nil { + return fmt.Errorf("failed to start utility VM (%s): %s", context, err) + } + return nil +} + +// terminateUvm terminates the service utility VM if its running. +func (d *Driver) terminateUvm(context string) error { + // Nothing to do if it's not running + if d.config.Uvm == nil { + return nil + } + + // FIXME: @jhowardmsft + // This isn't thread-safe yet, but will change anyway with the lifetime + // changes and multiple instances. Defering that work for now. + uvm := d.config.Uvm + d.config.Uvm = nil + + if err := uvm.Terminate(); err != nil { + return fmt.Errorf("failed to terminate utility VM (%s): %s", context, err) + } + + if err := uvm.WaitTimeout(time.Duration(d.config.UvmTimeoutSeconds) * time.Second); err != nil { + return fmt.Errorf("failed waiting for utility VM to terminate (%s): %s", context, err) + } + + return nil +} + +// String returns the string representation of a driver. This should match +// the name the graph driver has been registered with. +func (d *Driver) String() string { + return "lcow" +} + +// Status returns the status of the driver. +func (d *Driver) Status() [][2]string { + return [][2]string{ + {"LCOW", ""}, + } +} + +// Exists returns true if the given id is registered with this driver. +func (d *Driver) Exists(id string) bool { + _, err := os.Lstat(d.dir(id)) + logrus.Debugf("lcowdriver: exists: id %s %t", id, err == nil) + return err == nil +} + +// CreateReadWrite creates a layer that is writable for use as a container +// file system. That equates to creating a sandbox VHDx. +func (d *Driver) CreateReadWrite(id, parent string, opts *graphdriver.CreateOpts) error { + logrus.Debugf("lcowdriver: createreadwrite: id %s", id) + + if err := d.startUvm("createreadwrite"); err != nil { + return err + } + + if err := d.Create(id, parent, opts); err != nil { + return err + } + + return d.config.CreateSandbox(filepath.Join(d.dir(id), sandboxFilename), client.DefaultSandboxSizeMB, d.cachedSandboxFile) +} + +// Create creates a new read-only layer with the given id. +func (d *Driver) Create(id, parent string, opts *graphdriver.CreateOpts) error { + logrus.Debugf("lcowdriver: create: id %s parent: %s", id, parent) + + parentChain, err := d.getLayerChain(parent) + if err != nil { + return err + } + + var layerChain []string + if parent != "" { + if !d.Exists(parent) { + return fmt.Errorf("lcowdriver: cannot create read-only layer with missing parent %s", parent) + } + layerChain = []string{d.dir(parent)} + } + layerChain = append(layerChain, parentChain...) + + // Make sure layers are created with the correct ACL so that VMs can access them. + layerPath := d.dir(id) + logrus.Debugf("lcowdriver: create: id %s: creating layerPath %s", id, layerPath) + // Make sure the layers are created with the correct ACL so that VMs can access them. + if err := system.MkdirAllWithACL(layerPath, 755, system.SddlNtvmAdministratorsLocalSystem); err != nil { + return err + } + + if err := d.setLayerChain(id, layerChain); err != nil { + if err2 := os.RemoveAll(layerPath); err2 != nil { + logrus.Warnf("Failed to remove layer %s: %s", layerPath, err2) + } + return err + } + logrus.Debugf("lcowdriver: createreadwrite: id %s: success", id) + + return nil +} + +// Remove unmounts and removes the dir information. +func (d *Driver) Remove(id string) error { + logrus.Debugf("lcowdriver: remove: id %s", id) + tmpID := fmt.Sprintf("%s-removing", id) + tmpLayerPath := d.dir(tmpID) + layerPath := d.dir(id) + + logrus.Debugf("lcowdriver: remove: id %s: layerPath %s", id, layerPath) + if err := os.Rename(layerPath, tmpLayerPath); err != nil && !os.IsNotExist(err) { + return err + } + + if err := os.RemoveAll(tmpLayerPath); err != nil { + return err + } + + logrus.Debugf("lcowdriver: remove: id %s: layerPath %s succeeded", id, layerPath) + return nil +} + +// Get returns the rootfs path for the id. It is reference counted and +// effectively can be thought of as a "mount the layer into the utility +// vm if it isn't already" +func (d *Driver) Get(id, mountLabel string) (string, error) { + dir, _, _, err := d.getEx(id) + return dir, err +} + +// getEx is Get, but also returns the cache-entry and the size of the VHD +func (d *Driver) getEx(id string) (string, cacheType, int64, error) { + title := "lcowdriver: getEx" + logrus.Debugf("%s %s", title, id) + + if err := d.startUvm(fmt.Sprintf("getex %s", id)); err != nil { + logrus.Debugf("%s failed to start utility vm: %s", title, err) + return "", cacheType{}, 0, err + } + + // Work out what we are working on + vhdFilename, vhdSize, isSandbox, err := client.LayerVhdDetails(d.dir(id)) + if err != nil { + logrus.Debugf("%s failed to get LayerVhdDetails from %s: %s", title, d.dir(id), err) + return "", cacheType{}, 0, fmt.Errorf("%s failed to open layer or sandbox VHD to open in %s: %s", title, d.dir(id), err) + } + logrus.Debugf("%s %s, size %d, isSandbox %t", title, vhdFilename, vhdSize, isSandbox) + + hotAddRequired := false + d.cacheMu.Lock() + var cacheEntry cacheType + if _, ok := d.cache[id]; !ok { + // The item is not currently in the cache. + // + // Sandboxes need hot-adding in the case that there is a single global utility VM + // This will change for multiple instances with the lifetime changes. + if isSandbox { + hotAddRequired = true + } + d.cache[id] = cacheType{ + uvmPath: fmt.Sprintf("/mnt/%s", id), + refCount: 1, + isSandbox: isSandbox, + hostPath: vhdFilename, + } + } else { + // Increment the reference counter in the cache. + cacheEntry = d.cache[id] + cacheEntry.refCount++ + d.cache[id] = cacheEntry + } + + cacheEntry = d.cache[id] + logrus.Debugf("%s %s: isSandbox %t, refCount %d", title, id, cacheEntry.isSandbox, cacheEntry.refCount) + d.cacheMu.Unlock() + + if hotAddRequired { + logrus.Debugf("%s %s: Hot-Adding %s", title, id, vhdFilename) + if err := d.config.HotAddVhd(vhdFilename, cacheEntry.uvmPath); err != nil { + return "", cacheType{}, 0, fmt.Errorf("%s hot add %s failed: %s", title, vhdFilename, err) + } + } + + logrus.Debugf("%s %s success. %s: %+v: size %d", title, id, d.dir(id), cacheEntry, vhdSize) + return d.dir(id), cacheEntry, vhdSize, nil +} + +// Put does the reverse of get. If there are no more references to +// the layer, it unmounts it from the utility VM. +func (d *Driver) Put(id string) error { + title := "lcowdriver: put" + logrus.Debugf("%s %s", title, id) + + if err := d.startUvm(fmt.Sprintf("put %s", id)); err != nil { + return err + } + + d.cacheMu.Lock() + // Bad-news if unmounting something that isn't in the cache. + entry, ok := d.cache[id] + if !ok { + d.cacheMu.Unlock() + return fmt.Errorf("%s possible ref-count error, or invalid id was passed to the graphdriver. Cannot handle id %s as it's not in the cache", title, id) + } + + // Are we just decrementing the reference count + if entry.refCount > 1 { + entry.refCount-- + d.cache[id] = entry + logrus.Debugf("%s %s: refCount decremented to %d", title, id, entry.refCount) + d.cacheMu.Unlock() + return nil + } + + // No more references, so tear it down if previously hot-added + if entry.isSandbox { + logrus.Debugf("%s %s: Hot-Removing %s", title, id, entry.hostPath) + if err := d.config.HotRemoveVhd(entry.hostPath); err != nil { + d.cacheMu.Unlock() + return fmt.Errorf("%s failed to hot-remove %s from service utility VM: %s", title, entry.hostPath, err) + } + } + + // @jhowardmsft TEMPORARY FIX WHILE WAITING FOR HOT-REMOVE TO BE FIXED IN PLATFORM + //d.terminateUvm(fmt.Sprintf("put %s", id)) + + // Remove from the cache map. + delete(d.cache, id) + d.cacheMu.Unlock() + + logrus.Debugf("%s %s: refCount 0. %s (%s) completed successfully", title, id, entry.hostPath, entry.uvmPath) + return nil +} + +// Cleanup ensures the information the driver stores is properly removed. +// We use this opportunity to cleanup any -removing folders which may be +// still left if the daemon was killed while it was removing a layer. +func (d *Driver) Cleanup() error { + title := "lcowdriver: cleanup" + logrus.Debugf(title) + + d.cacheMu.Lock() + for k, v := range d.cache { + logrus.Debugf("%s cache entry: %s: %+v", title, k, v) + if v.refCount > 0 { + logrus.Warnf("%s leaked %s: %+v", title, k, v) + } + } + d.cacheMu.Unlock() + + items, err := ioutil.ReadDir(d.homeDir) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + + // Note we don't return an error below - it's possible the files + // are locked. However, next time around after the daemon exits, + // we likely will be able to to cleanup successfully. Instead we log + // warnings if there are errors. + for _, item := range items { + if item.IsDir() && strings.HasSuffix(item.Name(), "-removing") { + if err := os.RemoveAll(filepath.Join(d.homeDir, item.Name())); err != nil { + logrus.Warnf("%s failed to cleanup %s: %s", title, item.Name(), err) + } else { + logrus.Infof("%s cleaned up %s", title, item.Name()) + } + } + } + return nil +} + +// Diff takes a layer (and it's parent layer which may be null, but +// is ignored by this implementation below) and returns a reader for +// a tarstream representing the layers contents. The id could be +// a read-only "layer.vhd" or a read-write "sandbox.vhdx". The semantics +// of this function dictate that the layer is already mounted. +func (d *Driver) Diff(id, parent string) (io.ReadCloser, error) { + title := "lcowdriver: diff:" + logrus.Debugf("%s id %s", title, id) + + if err := d.startUvm(fmt.Sprintf("diff %s", id)); err != nil { + return nil, err + } + + d.cacheMu.Lock() + if _, ok := d.cache[id]; !ok { + d.cacheMu.Unlock() + return nil, fmt.Errorf("%s fail as %s is not in the cache", title, id) + } + cacheEntry := d.cache[id] + d.cacheMu.Unlock() + + // Stat to get size + fileInfo, err := os.Stat(cacheEntry.hostPath) + if err != nil { + return nil, fmt.Errorf("%s failed to stat %s: %s", title, cacheEntry.hostPath, err) + } + + // Then obtain the tar stream for it + logrus.Debugf("%s %s, size %d, isSandbox %t", title, cacheEntry.hostPath, fileInfo.Size(), cacheEntry.isSandbox) + tarReadCloser, err := d.config.VhdToTar(cacheEntry.hostPath, cacheEntry.uvmPath, cacheEntry.isSandbox, fileInfo.Size()) + if err != nil { + return nil, fmt.Errorf("%s failed to export layer to tar stream for id: %s, parent: %s : %s", title, id, parent, err) + } + logrus.Debugf("%s id %s parent %s completed successfully", title, id, parent) + return tarReadCloser, nil +} + +// ApplyDiff extracts the changeset from the given diff into the +// layer with the specified id and parent, returning the size of the +// new layer in bytes. The layer should not be mounted when calling +// this function. Another way of describing this is that ApplyDiff writes +// to a new layer (a VHD in LCOW) the contents of a tarstream it's given. +func (d *Driver) ApplyDiff(id, parent string, diff io.Reader) (int64, error) { + logrus.Debugf("lcowdriver: applydiff: id %s", id) + + if err := d.startUvm(fmt.Sprintf("applydiff %s", id)); err != nil { + return 0, err + } + + return d.config.TarToVhd(filepath.Join(d.homeDir, id, "layer.vhd"), diff) +} + +// Changes produces a list of changes between the specified layer +// and its parent layer. If parent is "", then all changes will be ADD changes. +// The layer should not be mounted when calling this function. +func (d *Driver) Changes(id, parent string) ([]archive.Change, error) { + logrus.Debugf("lcowdriver: changes: id %s parent %s", id, parent) + // TODO @gupta-ak. Needs implementation with assistance from service VM + return nil, nil +} + +// DiffSize calculates the changes between the specified layer +// and its parent and returns the size in bytes of the changes +// relative to its base filesystem directory. +func (d *Driver) DiffSize(id, parent string) (size int64, err error) { + logrus.Debugf("lcowdriver: diffsize: id %s", id) + // TODO @gupta-ak. Needs implementation with assistance from service VM + return 0, nil +} + +// GetMetadata returns custom driver information. +func (d *Driver) GetMetadata(id string) (map[string]string, error) { + logrus.Debugf("lcowdriver: getmetadata: id %s", id) + m := make(map[string]string) + m["dir"] = d.dir(id) + return m, nil +} + +// dir returns the absolute path to the layer. +func (d *Driver) dir(id string) string { + return filepath.Join(d.homeDir, filepath.Base(id)) +} + +// getLayerChain returns the layer chain information. +func (d *Driver) getLayerChain(id string) ([]string, error) { + jPath := filepath.Join(d.dir(id), "layerchain.json") + logrus.Debugf("lcowdriver: getlayerchain: id %s json %s", id, jPath) + content, err := ioutil.ReadFile(jPath) + if os.IsNotExist(err) { + return nil, nil + } else if err != nil { + return nil, fmt.Errorf("lcowdriver: getlayerchain: %s unable to read layerchain file %s: %s", id, jPath, err) + } + + var layerChain []string + err = json.Unmarshal(content, &layerChain) + if err != nil { + return nil, fmt.Errorf("lcowdriver: getlayerchain: %s failed to unmarshall layerchain file %s: %s", id, jPath, err) + } + return layerChain, nil +} + +// setLayerChain stores the layer chain information on disk. +func (d *Driver) setLayerChain(id string, chain []string) error { + content, err := json.Marshal(&chain) + if err != nil { + return fmt.Errorf("lcowdriver: setlayerchain: %s failed to marshall layerchain json: %s", id, err) + } + + jPath := filepath.Join(d.dir(id), "layerchain.json") + logrus.Debugf("lcowdriver: setlayerchain: id %s json %s", id, jPath) + err = ioutil.WriteFile(jPath, content, 0600) + if err != nil { + return fmt.Errorf("lcowdriver: setlayerchain: %s failed to write layerchain file: %s", id, err) + } + return nil +} diff --git a/daemon/graphdriver/register/register_windows.go b/daemon/graphdriver/register/register_windows.go index efaa5005ed..5bb1fd62a8 100644 --- a/daemon/graphdriver/register/register_windows.go +++ b/daemon/graphdriver/register/register_windows.go @@ -1,6 +1,7 @@ package register import ( - // register the windows graph driver + // register the windows graph drivers + _ "github.com/docker/docker/daemon/graphdriver/lcow" _ "github.com/docker/docker/daemon/graphdriver/windows" ) diff --git a/daemon/graphdriver/windows/windows.go b/daemon/graphdriver/windows/windows.go index 441621f6eb..a722078e35 100644 --- a/daemon/graphdriver/windows/windows.go +++ b/daemon/graphdriver/windows/windows.go @@ -94,6 +94,10 @@ func InitFilter(home string, options []string, uidMaps, gidMaps []idtools.IDMap) return nil, fmt.Errorf("%s is on an ReFS volume - ReFS volumes are not supported", home) } + if err := idtools.MkdirAllAs(home, 0700, 0, 0); err != nil { + return nil, fmt.Errorf("windowsfilter failed to create '%s': %v", home, err) + } + d := &Driver{ info: hcsshim.DriverInfo{ HomeDir: home, @@ -149,8 +153,19 @@ func (d *Driver) Status() [][2]string { } } +// panicIfUsedByLcow does exactly what it says. +// TODO @jhowardmsft - this is an temporary measure for the bring-up of +// Linux containers on Windows. It is a failsafe to ensure that the right +// graphdriver is used. +func panicIfUsedByLcow() { + if system.LCOWSupported() { + panic("inconsistency - windowsfilter graphdriver should not be used when in LCOW mode") + } +} + // Exists returns true if the given id is registered with this driver. func (d *Driver) Exists(id string) bool { + panicIfUsedByLcow() rID, err := d.resolveID(id) if err != nil { return false @@ -165,6 +180,7 @@ func (d *Driver) Exists(id string) bool { // CreateReadWrite creates a layer that is writable for use as a container // file system. func (d *Driver) CreateReadWrite(id, parent string, opts *graphdriver.CreateOpts) error { + panicIfUsedByLcow() if opts != nil { return d.create(id, parent, opts.MountLabel, false, opts.StorageOpt) } @@ -173,6 +189,7 @@ func (d *Driver) CreateReadWrite(id, parent string, opts *graphdriver.CreateOpts // Create creates a new read-only layer with the given id. func (d *Driver) Create(id, parent string, opts *graphdriver.CreateOpts) error { + panicIfUsedByLcow() if opts != nil { return d.create(id, parent, opts.MountLabel, true, opts.StorageOpt) } @@ -256,6 +273,7 @@ func (d *Driver) dir(id string) string { // Remove unmounts and removes the dir information. func (d *Driver) Remove(id string) error { + panicIfUsedByLcow() rID, err := d.resolveID(id) if err != nil { return err @@ -337,6 +355,7 @@ func (d *Driver) Remove(id string) error { // Get returns the rootfs path for the id. This will mount the dir at its given path. func (d *Driver) Get(id, mountLabel string) (string, error) { + panicIfUsedByLcow() logrus.Debugf("WindowsGraphDriver Get() id %s mountLabel %s", id, mountLabel) var dir string @@ -395,6 +414,7 @@ func (d *Driver) Get(id, mountLabel string) (string, error) { // Put adds a new layer to the driver. func (d *Driver) Put(id string) error { + panicIfUsedByLcow() logrus.Debugf("WindowsGraphDriver Put() id %s", id) rID, err := d.resolveID(id) @@ -424,7 +444,6 @@ func (d *Driver) Put(id string) error { // We use this opportunity to cleanup any -removing folders which may be // still left if the daemon was killed while it was removing a layer. func (d *Driver) Cleanup() error { - items, err := ioutil.ReadDir(d.info.HomeDir) if err != nil { if os.IsNotExist(err) { @@ -454,6 +473,7 @@ func (d *Driver) Cleanup() error { // layer and its parent layer which may be "". // The layer should be mounted when calling this function func (d *Driver) Diff(id, parent string) (_ io.ReadCloser, err error) { + panicIfUsedByLcow() rID, err := d.resolveID(id) if err != nil { return @@ -490,6 +510,7 @@ func (d *Driver) Diff(id, parent string) (_ io.ReadCloser, err error) { // and its parent layer. If parent is "", then all changes will be ADD changes. // The layer should not be mounted when calling this function. func (d *Driver) Changes(id, parent string) ([]archive.Change, error) { + panicIfUsedByLcow() rID, err := d.resolveID(id) if err != nil { return nil, err @@ -545,6 +566,7 @@ func (d *Driver) Changes(id, parent string) ([]archive.Change, error) { // new layer in bytes. // The layer should not be mounted when calling this function func (d *Driver) ApplyDiff(id, parent string, diff io.Reader) (int64, error) { + panicIfUsedByLcow() var layerChain []string if parent != "" { rPId, err := d.resolveID(parent) @@ -579,6 +601,7 @@ func (d *Driver) ApplyDiff(id, parent string, diff io.Reader) (int64, error) { // and its parent and returns the size in bytes of the changes // relative to its base filesystem directory. func (d *Driver) DiffSize(id, parent string) (size int64, err error) { + panicIfUsedByLcow() rPId, err := d.resolveID(parent) if err != nil { return @@ -600,6 +623,7 @@ func (d *Driver) DiffSize(id, parent string) (size int64, err error) { // GetMetadata returns custom driver information. func (d *Driver) GetMetadata(id string) (map[string]string, error) { + panicIfUsedByLcow() m := make(map[string]string) m["dir"] = d.dir(id) return m, nil @@ -902,6 +926,7 @@ func (fg *fileGetCloserWithBackupPrivileges) Close() error { // DiffGetter returns a FileGetCloser that can read files from the directory that // contains files for the layer differences. Used for direct access for tar-split. func (d *Driver) DiffGetter(id string) (graphdriver.FileGetCloser, error) { + panicIfUsedByLcow() id, err := d.resolveID(id) if err != nil { return nil, err diff --git a/daemon/health_test.go b/daemon/health_test.go index fbbdf06c52..7347e7d791 100644 --- a/daemon/health_test.go +++ b/daemon/health_test.go @@ -19,17 +19,15 @@ func reset(c *container.Container) { func TestNoneHealthcheck(t *testing.T) { c := &container.Container{ - CommonContainer: container.CommonContainer{ - ID: "container_id", - Name: "container_name", - Config: &containertypes.Config{ - Image: "image_name", - Healthcheck: &containertypes.HealthConfig{ - Test: []string{"NONE"}, - }, + ID: "container_id", + Name: "container_name", + Config: &containertypes.Config{ + Image: "image_name", + Healthcheck: &containertypes.HealthConfig{ + Test: []string{"NONE"}, }, - State: &container.State{}, }, + State: &container.State{}, } daemon := &Daemon{} @@ -58,12 +56,10 @@ func TestHealthStates(t *testing.T) { } c := &container.Container{ - CommonContainer: container.CommonContainer{ - ID: "container_id", - Name: "container_name", - Config: &containertypes.Config{ - Image: "image_name", - }, + ID: "container_id", + Name: "container_name", + Config: &containertypes.Config{ + Image: "image_name", }, } daemon := &Daemon{ diff --git a/daemon/image.go b/daemon/image.go index d10457118f..a51049dbb5 100644 --- a/daemon/image.go +++ b/daemon/image.go @@ -21,37 +21,43 @@ func (e ErrImageDoesNotExist) Error() string { return fmt.Sprintf("No such image: %s", reference.FamiliarString(ref)) } -// GetImageID returns an image ID corresponding to the image referred to by +// GetImageIDAndPlatform returns an image ID and platform corresponding to the image referred to by // refOrID. -func (daemon *Daemon) GetImageID(refOrID string) (image.ID, error) { +func (daemon *Daemon) GetImageIDAndPlatform(refOrID string) (image.ID, string, error) { ref, err := reference.ParseAnyReference(refOrID) if err != nil { - return "", err + return "", "", err } namedRef, ok := ref.(reference.Named) if !ok { digested, ok := ref.(reference.Digested) if !ok { - return "", ErrImageDoesNotExist{ref} + return "", "", ErrImageDoesNotExist{ref} } id := image.IDFromDigest(digested.Digest()) - if _, err := daemon.imageStore.Get(id); err != nil { - return "", ErrImageDoesNotExist{ref} + for platform := range daemon.stores { + if _, err = daemon.stores[platform].imageStore.Get(id); err == nil { + return id, platform, nil + } } - return id, nil + return "", "", ErrImageDoesNotExist{ref} } - if id, err := daemon.referenceStore.Get(namedRef); err == nil { - return image.IDFromDigest(id), nil + for platform := range daemon.stores { + if id, err := daemon.stores[platform].referenceStore.Get(namedRef); err == nil { + return image.IDFromDigest(id), platform, nil + } } // deprecated: repo:shortid https://github.com/docker/docker/pull/799 if tagged, ok := namedRef.(reference.Tagged); ok { if tag := tagged.Tag(); stringid.IsShortID(stringid.TruncateID(tag)) { - if id, err := daemon.imageStore.Search(tag); err == nil { - for _, storeRef := range daemon.referenceStore.References(id.Digest()) { - if storeRef.Name() == namedRef.Name() { - return id, nil + for platform := range daemon.stores { + if id, err := daemon.stores[platform].imageStore.Search(tag); err == nil { + for _, storeRef := range daemon.stores[platform].referenceStore.References(id.Digest()) { + if storeRef.Name() == namedRef.Name() { + return id, platform, nil + } } } } @@ -59,18 +65,20 @@ func (daemon *Daemon) GetImageID(refOrID string) (image.ID, error) { } // Search based on ID - if id, err := daemon.imageStore.Search(refOrID); err == nil { - return id, nil + for platform := range daemon.stores { + if id, err := daemon.stores[platform].imageStore.Search(refOrID); err == nil { + return id, platform, nil + } } - return "", ErrImageDoesNotExist{ref} + return "", "", ErrImageDoesNotExist{ref} } // GetImage returns an image corresponding to the image referred to by refOrID. func (daemon *Daemon) GetImage(refOrID string) (*image.Image, error) { - imgID, err := daemon.GetImageID(refOrID) + imgID, platform, err := daemon.GetImageIDAndPlatform(refOrID) if err != nil { return nil, err } - return daemon.imageStore.Get(imgID) + return daemon.stores[platform].imageStore.Get(imgID) } diff --git a/daemon/image_delete.go b/daemon/image_delete.go index b7dbd249eb..4e228594bc 100644 --- a/daemon/image_delete.go +++ b/daemon/image_delete.go @@ -65,12 +65,12 @@ func (daemon *Daemon) ImageDelete(imageRef string, force, prune bool) ([]types.I start := time.Now() records := []types.ImageDeleteResponseItem{} - imgID, err := daemon.GetImageID(imageRef) + imgID, platform, err := daemon.GetImageIDAndPlatform(imageRef) if err != nil { return nil, daemon.imageNotExistToErrcode(err) } - repoRefs := daemon.referenceStore.References(imgID.Digest()) + repoRefs := daemon.stores[platform].referenceStore.References(imgID.Digest()) var removedRepositoryRef bool if !isImageIDPrefix(imgID.String(), imageRef) { @@ -94,7 +94,7 @@ func (daemon *Daemon) ImageDelete(imageRef string, force, prune bool) ([]types.I return nil, err } - parsedRef, err = daemon.removeImageRef(parsedRef) + parsedRef, err = daemon.removeImageRef(platform, parsedRef) if err != nil { return nil, err } @@ -104,7 +104,7 @@ func (daemon *Daemon) ImageDelete(imageRef string, force, prune bool) ([]types.I daemon.LogImageEvent(imgID.String(), imgID.String(), "untag") records = append(records, untaggedRecord) - repoRefs = daemon.referenceStore.References(imgID.Digest()) + repoRefs = daemon.stores[platform].referenceStore.References(imgID.Digest()) // If a tag reference was removed and the only remaining // references to the same repository are digest references, @@ -122,7 +122,7 @@ func (daemon *Daemon) ImageDelete(imageRef string, force, prune bool) ([]types.I remainingRefs := []reference.Named{} for _, repoRef := range repoRefs { if _, repoRefIsCanonical := repoRef.(reference.Canonical); repoRefIsCanonical && parsedRef.Name() == repoRef.Name() { - if _, err := daemon.removeImageRef(repoRef); err != nil { + if _, err := daemon.removeImageRef(platform, repoRef); err != nil { return records, err } @@ -152,12 +152,12 @@ func (daemon *Daemon) ImageDelete(imageRef string, force, prune bool) ([]types.I if !force { c |= conflictSoft &^ conflictActiveReference } - if conflict := daemon.checkImageDeleteConflict(imgID, c); conflict != nil { + if conflict := daemon.checkImageDeleteConflict(imgID, platform, c); conflict != nil { return nil, conflict } for _, repoRef := range repoRefs { - parsedRef, err := daemon.removeImageRef(repoRef) + parsedRef, err := daemon.removeImageRef(platform, repoRef) if err != nil { return nil, err } @@ -170,7 +170,7 @@ func (daemon *Daemon) ImageDelete(imageRef string, force, prune bool) ([]types.I } } - if err := daemon.imageDeleteHelper(imgID, &records, force, prune, removedRepositoryRef); err != nil { + if err := daemon.imageDeleteHelper(imgID, platform, &records, force, prune, removedRepositoryRef); err != nil { return nil, err } @@ -231,13 +231,13 @@ func (daemon *Daemon) getContainerUsingImage(imageID image.ID) *container.Contai // repositoryRef must not be an image ID but a repository name followed by an // optional tag or digest reference. If tag or digest is omitted, the default // tag is used. Returns the resolved image reference and an error. -func (daemon *Daemon) removeImageRef(ref reference.Named) (reference.Named, error) { +func (daemon *Daemon) removeImageRef(platform string, ref reference.Named) (reference.Named, error) { ref = reference.TagNameOnly(ref) // Ignore the boolean value returned, as far as we're concerned, this // is an idempotent operation and it's okay if the reference didn't // exist in the first place. - _, err := daemon.referenceStore.Delete(ref) + _, err := daemon.stores[platform].referenceStore.Delete(ref) return ref, err } @@ -247,11 +247,11 @@ func (daemon *Daemon) removeImageRef(ref reference.Named) (reference.Named, erro // on the first encountered error. Removed references are logged to this // daemon's event service. An "Untagged" types.ImageDeleteResponseItem is added to the // given list of records. -func (daemon *Daemon) removeAllReferencesToImageID(imgID image.ID, records *[]types.ImageDeleteResponseItem) error { - imageRefs := daemon.referenceStore.References(imgID.Digest()) +func (daemon *Daemon) removeAllReferencesToImageID(imgID image.ID, platform string, records *[]types.ImageDeleteResponseItem) error { + imageRefs := daemon.stores[platform].referenceStore.References(imgID.Digest()) for _, imageRef := range imageRefs { - parsedRef, err := daemon.removeImageRef(imageRef) + parsedRef, err := daemon.removeImageRef(platform, imageRef) if err != nil { return err } @@ -296,15 +296,15 @@ func (idc *imageDeleteConflict) Error() string { // conflict is encountered, it will be returned immediately without deleting // the image. If quiet is true, any encountered conflicts will be ignored and // the function will return nil immediately without deleting the image. -func (daemon *Daemon) imageDeleteHelper(imgID image.ID, records *[]types.ImageDeleteResponseItem, force, prune, quiet bool) error { +func (daemon *Daemon) imageDeleteHelper(imgID image.ID, platform string, records *[]types.ImageDeleteResponseItem, force, prune, quiet bool) error { // First, determine if this image has any conflicts. Ignore soft conflicts // if force is true. c := conflictHard if !force { c |= conflictSoft } - if conflict := daemon.checkImageDeleteConflict(imgID, c); conflict != nil { - if quiet && (!daemon.imageIsDangling(imgID) || conflict.used) { + if conflict := daemon.checkImageDeleteConflict(imgID, platform, c); conflict != nil { + if quiet && (!daemon.imageIsDangling(imgID, platform) || conflict.used) { // Ignore conflicts UNLESS the image is "dangling" or not being used in // which case we want the user to know. return nil @@ -315,18 +315,18 @@ func (daemon *Daemon) imageDeleteHelper(imgID image.ID, records *[]types.ImageDe return conflict } - parent, err := daemon.imageStore.GetParent(imgID) + parent, err := daemon.stores[platform].imageStore.GetParent(imgID) if err != nil { // There may be no parent parent = "" } // Delete all repository tag/digest references to this image. - if err := daemon.removeAllReferencesToImageID(imgID, records); err != nil { + if err := daemon.removeAllReferencesToImageID(imgID, platform, records); err != nil { return err } - removedLayers, err := daemon.imageStore.Delete(imgID) + removedLayers, err := daemon.stores[platform].imageStore.Delete(imgID) if err != nil { return err } @@ -346,7 +346,7 @@ func (daemon *Daemon) imageDeleteHelper(imgID image.ID, records *[]types.ImageDe // either running or stopped). // Do not force prunings, but do so quietly (stopping on any encountered // conflicts). - return daemon.imageDeleteHelper(parent, records, false, true, true) + return daemon.imageDeleteHelper(parent, platform, records, false, true, true) } // checkImageDeleteConflict determines whether there are any conflicts @@ -355,9 +355,9 @@ func (daemon *Daemon) imageDeleteHelper(imgID image.ID, records *[]types.ImageDe // using the image. A soft conflict is any tags/digest referencing the given // image or any stopped container using the image. If ignoreSoftConflicts is // true, this function will not check for soft conflict conditions. -func (daemon *Daemon) checkImageDeleteConflict(imgID image.ID, mask conflictType) *imageDeleteConflict { +func (daemon *Daemon) checkImageDeleteConflict(imgID image.ID, platform string, mask conflictType) *imageDeleteConflict { // Check if the image has any descendant images. - if mask&conflictDependentChild != 0 && len(daemon.imageStore.Children(imgID)) > 0 { + if mask&conflictDependentChild != 0 && len(daemon.stores[platform].imageStore.Children(imgID)) > 0 { return &imageDeleteConflict{ hard: true, imgID: imgID, @@ -381,7 +381,7 @@ func (daemon *Daemon) checkImageDeleteConflict(imgID image.ID, mask conflictType } // Check if any repository tags/digest reference this image. - if mask&conflictActiveReference != 0 && len(daemon.referenceStore.References(imgID.Digest())) > 0 { + if mask&conflictActiveReference != 0 && len(daemon.stores[platform].referenceStore.References(imgID.Digest())) > 0 { return &imageDeleteConflict{ imgID: imgID, message: "image is referenced in multiple repositories", @@ -408,6 +408,6 @@ func (daemon *Daemon) checkImageDeleteConflict(imgID image.ID, mask conflictType // imageIsDangling returns whether the given image is "dangling" which means // that there are no repository references to the given image and it has no // child images. -func (daemon *Daemon) imageIsDangling(imgID image.ID) bool { - return !(len(daemon.referenceStore.References(imgID.Digest())) > 0 || len(daemon.imageStore.Children(imgID)) > 0) +func (daemon *Daemon) imageIsDangling(imgID image.ID, platform string) bool { + return !(len(daemon.stores[platform].referenceStore.References(imgID.Digest())) > 0 || len(daemon.stores[platform].imageStore.Children(imgID)) > 0) } diff --git a/daemon/image_exporter.go b/daemon/image_exporter.go index 95d1d3dcdb..edac775d43 100644 --- a/daemon/image_exporter.go +++ b/daemon/image_exporter.go @@ -2,8 +2,10 @@ package daemon import ( "io" + "runtime" "github.com/docker/docker/image/tarexport" + "github.com/docker/docker/pkg/system" ) // ExportImage exports a list of images to the given output stream. The @@ -12,7 +14,12 @@ import ( // the same tag are exported. names is the set of tags to export, and // outStream is the writer which the images are written to. func (daemon *Daemon) ExportImage(names []string, outStream io.Writer) error { - imageExporter := tarexport.NewTarExporter(daemon.imageStore, daemon.layerStore, daemon.referenceStore, daemon) + // TODO @jhowardmsft LCOW. This will need revisiting later. + platform := runtime.GOOS + if platform == "windows" && system.LCOWSupported() { + platform = "linux" + } + imageExporter := tarexport.NewTarExporter(daemon.stores[platform].imageStore, daemon.stores[platform].layerStore, daemon.stores[platform].referenceStore, daemon) return imageExporter.Save(names, outStream) } @@ -20,6 +27,11 @@ func (daemon *Daemon) ExportImage(names []string, outStream io.Writer) error { // complement of ImageExport. The input stream is an uncompressed tar // ball containing images and metadata. func (daemon *Daemon) LoadImage(inTar io.ReadCloser, outStream io.Writer, quiet bool) error { - imageExporter := tarexport.NewTarExporter(daemon.imageStore, daemon.layerStore, daemon.referenceStore, daemon) + // TODO @jhowardmsft LCOW. This will need revisiting later. + platform := runtime.GOOS + if platform == "windows" && system.LCOWSupported() { + platform = "linux" + } + imageExporter := tarexport.NewTarExporter(daemon.stores[platform].imageStore, daemon.stores[platform].layerStore, daemon.stores[platform].referenceStore, daemon) return imageExporter.Load(inTar, outStream, quiet) } diff --git a/daemon/image_history.go b/daemon/image_history.go index b763c86c03..c9e81554e9 100644 --- a/daemon/image_history.go +++ b/daemon/image_history.go @@ -2,6 +2,7 @@ package daemon import ( "fmt" + "runtime" "time" "github.com/docker/distribution/reference" @@ -18,6 +19,12 @@ func (daemon *Daemon) ImageHistory(name string) ([]*image.HistoryResponseItem, e return nil, err } + // If the image OS isn't set, assume it's the host OS + platform := img.OS + if platform == "" { + platform = runtime.GOOS + } + history := []*image.HistoryResponseItem{} layerCounter := 0 @@ -33,12 +40,12 @@ func (daemon *Daemon) ImageHistory(name string) ([]*image.HistoryResponseItem, e } rootFS.Append(img.RootFS.DiffIDs[layerCounter]) - l, err := daemon.layerStore.Get(rootFS.ChainID()) + l, err := daemon.stores[platform].layerStore.Get(rootFS.ChainID()) if err != nil { return nil, err } layerSize, err = l.DiffSize() - layer.ReleaseAndLog(daemon.layerStore, l) + layer.ReleaseAndLog(daemon.stores[platform].layerStore, l) if err != nil { return nil, err } @@ -62,7 +69,7 @@ func (daemon *Daemon) ImageHistory(name string) ([]*image.HistoryResponseItem, e h.ID = id.String() var tags []string - for _, r := range daemon.referenceStore.References(id.Digest()) { + for _, r := range daemon.stores[platform].referenceStore.References(id.Digest()) { if _, ok := r.(reference.NamedTagged); ok { tags = append(tags, reference.FamiliarString(r)) } diff --git a/daemon/image_inspect.go b/daemon/image_inspect.go index 267a41946a..672a4cc012 100644 --- a/daemon/image_inspect.go +++ b/daemon/image_inspect.go @@ -1,6 +1,7 @@ package daemon import ( + "runtime" "time" "github.com/docker/distribution/reference" @@ -17,7 +18,13 @@ func (daemon *Daemon) LookupImage(name string) (*types.ImageInspect, error) { return nil, errors.Wrapf(err, "no such image: %s", name) } - refs := daemon.referenceStore.References(img.ID().Digest()) + // If the image OS isn't set, assume it's the host OS + platform := img.OS + if platform == "" { + platform = runtime.GOOS + } + + refs := daemon.stores[platform].referenceStore.References(img.ID().Digest()) repoTags := []string{} repoDigests := []string{} for _, ref := range refs { @@ -33,11 +40,11 @@ func (daemon *Daemon) LookupImage(name string) (*types.ImageInspect, error) { var layerMetadata map[string]string layerID := img.RootFS.ChainID() if layerID != "" { - l, err := daemon.layerStore.Get(layerID) + l, err := daemon.stores[platform].layerStore.Get(layerID) if err != nil { return nil, err } - defer layer.ReleaseAndLog(daemon.layerStore, l) + defer layer.ReleaseAndLog(daemon.stores[platform].layerStore, l) size, err = l.Size() if err != nil { return nil, err @@ -67,15 +74,14 @@ func (daemon *Daemon) LookupImage(name string) (*types.ImageInspect, error) { Author: img.Author, Config: img.Config, Architecture: img.Architecture, - Os: img.OS, + Os: platform, OsVersion: img.OSVersion, Size: size, VirtualSize: size, // TODO: field unused, deprecate RootFS: rootFSToAPIType(img.RootFS), } - imageInspect.GraphDriver.Name = daemon.GraphDriverName() - + imageInspect.GraphDriver.Name = daemon.GraphDriverName(platform) imageInspect.GraphDriver.Data = layerMetadata return imageInspect, nil diff --git a/daemon/image_pull.go b/daemon/image_pull.go index 304fd9f024..abc81ec67c 100644 --- a/daemon/image_pull.go +++ b/daemon/image_pull.go @@ -2,6 +2,7 @@ package daemon import ( "io" + "runtime" "strings" dist "github.com/docker/distribution" @@ -17,7 +18,7 @@ import ( // PullImage initiates a pull operation. image is the repository name to pull, and // tag may be either empty, or indicate a specific tag to pull. -func (daemon *Daemon) PullImage(ctx context.Context, image, tag string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error { +func (daemon *Daemon) PullImage(ctx context.Context, image, tag, platform string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error { // Special case: "pull -a" may send an image name with a // trailing :. This is ugly, but let's not break API // compatibility. @@ -42,10 +43,10 @@ func (daemon *Daemon) PullImage(ctx context.Context, image, tag string, metaHead } } - return daemon.pullImageWithReference(ctx, ref, metaHeaders, authConfig, outStream) + return daemon.pullImageWithReference(ctx, ref, platform, metaHeaders, authConfig, outStream) } -func (daemon *Daemon) pullImageWithReference(ctx context.Context, ref reference.Named, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error { +func (daemon *Daemon) pullImageWithReference(ctx context.Context, ref reference.Named, platform string, metaHeaders map[string][]string, authConfig *types.AuthConfig, outStream io.Writer) error { // Include a buffer so that slow client connections don't affect // transfer performance. progressChan := make(chan progress.Progress, 100) @@ -59,6 +60,11 @@ func (daemon *Daemon) pullImageWithReference(ctx context.Context, ref reference. close(writesDone) }() + // Default to the host OS platform in case it hasn't been populated with an explicit value. + if platform == "" { + platform = runtime.GOOS + } + imagePullConfig := &distribution.ImagePullConfig{ Config: distribution.Config{ MetaHeaders: metaHeaders, @@ -66,12 +72,13 @@ func (daemon *Daemon) pullImageWithReference(ctx context.Context, ref reference. ProgressOutput: progress.ChanOutput(progressChan), RegistryService: daemon.RegistryService, ImageEventLogger: daemon.LogImageEvent, - MetadataStore: daemon.distributionMetadataStore, - ImageStore: distribution.NewImageConfigStoreFromStore(daemon.imageStore), - ReferenceStore: daemon.referenceStore, + MetadataStore: daemon.stores[platform].distributionMetadataStore, + ImageStore: distribution.NewImageConfigStoreFromStore(daemon.stores[platform].imageStore), + ReferenceStore: daemon.stores[platform].referenceStore, }, DownloadManager: daemon.downloadManager, Schema2Types: distribution.ImageTypes, + Platform: platform, } err := distribution.Pull(ctx, ref, imagePullConfig) diff --git a/daemon/image_push.go b/daemon/image_push.go index 0f060d117f..b1d15454fb 100644 --- a/daemon/image_push.go +++ b/daemon/image_push.go @@ -2,6 +2,7 @@ package daemon import ( "io" + "runtime" "github.com/docker/distribution/manifest/schema2" "github.com/docker/distribution/reference" @@ -9,6 +10,7 @@ import ( "github.com/docker/docker/distribution" progressutils "github.com/docker/docker/distribution/utils" "github.com/docker/docker/pkg/progress" + "github.com/docker/docker/pkg/system" "golang.org/x/net/context" ) @@ -39,6 +41,12 @@ func (daemon *Daemon) PushImage(ctx context.Context, image, tag string, metaHead close(writesDone) }() + // TODO @jhowardmsft LCOW Support. This will require revisiting. For now, hard-code. + platform := runtime.GOOS + if platform == "windows" && system.LCOWSupported() { + platform = "linux" + } + imagePushConfig := &distribution.ImagePushConfig{ Config: distribution.Config{ MetaHeaders: metaHeaders, @@ -46,12 +54,12 @@ func (daemon *Daemon) PushImage(ctx context.Context, image, tag string, metaHead ProgressOutput: progress.ChanOutput(progressChan), RegistryService: daemon.RegistryService, ImageEventLogger: daemon.LogImageEvent, - MetadataStore: daemon.distributionMetadataStore, - ImageStore: distribution.NewImageConfigStoreFromStore(daemon.imageStore), - ReferenceStore: daemon.referenceStore, + MetadataStore: daemon.stores[platform].distributionMetadataStore, + ImageStore: distribution.NewImageConfigStoreFromStore(daemon.stores[platform].imageStore), + ReferenceStore: daemon.stores[platform].referenceStore, }, ConfigMediaType: schema2.MediaTypeImageConfig, - LayerStore: distribution.NewLayerProviderFromStore(daemon.layerStore), + LayerStore: distribution.NewLayerProviderFromStore(daemon.stores[platform].layerStore), TrustKey: daemon.trustKey, UploadManager: daemon.uploadManager, } diff --git a/daemon/image_tag.go b/daemon/image_tag.go index 10a584b361..199ca9ad84 100644 --- a/daemon/image_tag.go +++ b/daemon/image_tag.go @@ -8,7 +8,7 @@ import ( // TagImage creates the tag specified by newTag, pointing to the image named // imageName (alternatively, imageName can also be an image ID). func (daemon *Daemon) TagImage(imageName, repository, tag string) error { - imageID, err := daemon.GetImageID(imageName) + imageID, platform, err := daemon.GetImageIDAndPlatform(imageName) if err != nil { return err } @@ -23,12 +23,12 @@ func (daemon *Daemon) TagImage(imageName, repository, tag string) error { } } - return daemon.TagImageWithReference(imageID, newTag) + return daemon.TagImageWithReference(imageID, platform, newTag) } // TagImageWithReference adds the given reference to the image ID provided. -func (daemon *Daemon) TagImageWithReference(imageID image.ID, newTag reference.Named) error { - if err := daemon.referenceStore.AddTag(newTag, imageID.Digest(), true); err != nil { +func (daemon *Daemon) TagImageWithReference(imageID image.ID, platform string, newTag reference.Named) error { + if err := daemon.stores[platform].referenceStore.AddTag(newTag, imageID.Digest(), true); err != nil { return err } diff --git a/daemon/images.go b/daemon/images.go index b014fd4887..6e29cae4d0 100644 --- a/daemon/images.go +++ b/daemon/images.go @@ -3,6 +3,7 @@ package daemon import ( "encoding/json" "fmt" + "runtime" "sort" "time" @@ -14,6 +15,7 @@ import ( "github.com/docker/docker/container" "github.com/docker/docker/image" "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/system" ) var acceptedImageFilterTags = map[string]bool{ @@ -34,7 +36,12 @@ func (r byCreated) Less(i, j int) bool { return r[i].Created < r[j].Created } // Map returns a map of all images in the ImageStore func (daemon *Daemon) Map() map[image.ID]*image.Image { - return daemon.imageStore.Map() + // TODO @jhowardmsft LCOW. This will need work to enumerate the stores for all platforms. + platform := runtime.GOOS + if platform == "windows" && system.LCOWSupported() { + platform = "linux" + } + return daemon.stores[platform].imageStore.Map() } // Images returns a filtered list of images. filterArgs is a JSON-encoded set @@ -43,6 +50,13 @@ func (daemon *Daemon) Map() map[image.ID]*image.Image { // named all controls whether all images in the graph are filtered, or just // the heads. func (daemon *Daemon) Images(imageFilters filters.Args, all bool, withExtraAttrs bool) ([]*types.ImageSummary, error) { + + // TODO @jhowardmsft LCOW. This will need work to enumerate the stores for all platforms. + platform := runtime.GOOS + if platform == "windows" && system.LCOWSupported() { + platform = "linux" + } + var ( allImages map[image.ID]*image.Image err error @@ -61,9 +75,9 @@ func (daemon *Daemon) Images(imageFilters filters.Args, all bool, withExtraAttrs } } if danglingOnly { - allImages = daemon.imageStore.Heads() + allImages = daemon.stores[platform].imageStore.Heads() } else { - allImages = daemon.imageStore.Map() + allImages = daemon.stores[platform].imageStore.Map() } var beforeFilter, sinceFilter *image.Image @@ -116,7 +130,7 @@ func (daemon *Daemon) Images(imageFilters filters.Args, all bool, withExtraAttrs layerID := img.RootFS.ChainID() var size int64 if layerID != "" { - l, err := daemon.layerStore.Get(layerID) + l, err := daemon.stores[platform].layerStore.Get(layerID) if err != nil { // The layer may have been deleted between the call to `Map()` or // `Heads()` and the call to `Get()`, so we just ignore this error @@ -127,7 +141,7 @@ func (daemon *Daemon) Images(imageFilters filters.Args, all bool, withExtraAttrs } size, err = l.Size() - layer.ReleaseAndLog(daemon.layerStore, l) + layer.ReleaseAndLog(daemon.stores[platform].layerStore, l) if err != nil { return nil, err } @@ -135,7 +149,7 @@ func (daemon *Daemon) Images(imageFilters filters.Args, all bool, withExtraAttrs newImage := newImage(img, size) - for _, ref := range daemon.referenceStore.References(id.Digest()) { + for _, ref := range daemon.stores[platform].referenceStore.References(id.Digest()) { if imageFilters.Include("reference") { var found bool var matchErr error @@ -157,7 +171,7 @@ func (daemon *Daemon) Images(imageFilters filters.Args, all bool, withExtraAttrs } } if newImage.RepoDigests == nil && newImage.RepoTags == nil { - if all || len(daemon.imageStore.Children(id)) == 0 { + if all || len(daemon.stores[platform].imageStore.Children(id)) == 0 { if imageFilters.Include("dangling") && !danglingOnly { //dangling=false case, so dangling image is not needed @@ -179,7 +193,7 @@ func (daemon *Daemon) Images(imageFilters filters.Args, all bool, withExtraAttrs // lazily init variables if imagesMap == nil { allContainers = daemon.List() - allLayers = daemon.layerStore.Map() + allLayers = daemon.stores[platform].layerStore.Map() imagesMap = make(map[*image.Image]*types.ImageSummary) layerRefs = make(map[layer.ChainID]int) } @@ -242,7 +256,16 @@ func (daemon *Daemon) Images(imageFilters filters.Args, all bool, withExtraAttrs // The existing image(s) is not destroyed. // If no parent is specified, a new image with the diff of all the specified image's layers merged into a new layer that has no parents. func (daemon *Daemon) SquashImage(id, parent string) (string, error) { - img, err := daemon.imageStore.Get(image.ID(id)) + + var ( + img *image.Image + err error + ) + for _, ds := range daemon.stores { + if img, err = ds.imageStore.Get(image.ID(id)); err == nil { + break + } + } if err != nil { return "", err } @@ -250,7 +273,7 @@ func (daemon *Daemon) SquashImage(id, parent string) (string, error) { var parentImg *image.Image var parentChainID layer.ChainID if len(parent) != 0 { - parentImg, err = daemon.imageStore.Get(image.ID(parent)) + parentImg, err = daemon.stores[img.Platform()].imageStore.Get(image.ID(parent)) if err != nil { return "", errors.Wrap(err, "error getting specified parent layer") } @@ -260,11 +283,11 @@ func (daemon *Daemon) SquashImage(id, parent string) (string, error) { parentImg = &image.Image{RootFS: rootFS} } - l, err := daemon.layerStore.Get(img.RootFS.ChainID()) + l, err := daemon.stores[img.Platform()].layerStore.Get(img.RootFS.ChainID()) if err != nil { return "", errors.Wrap(err, "error getting image layer") } - defer daemon.layerStore.Release(l) + defer daemon.stores[img.Platform()].layerStore.Release(l) ts, err := l.TarStreamFrom(parentChainID) if err != nil { @@ -272,11 +295,11 @@ func (daemon *Daemon) SquashImage(id, parent string) (string, error) { } defer ts.Close() - newL, err := daemon.layerStore.Register(ts, parentChainID) + newL, err := daemon.stores[img.Platform()].layerStore.Register(ts, parentChainID, layer.Platform(img.Platform())) if err != nil { return "", errors.Wrap(err, "error registering layer") } - defer daemon.layerStore.Release(newL) + defer daemon.stores[img.Platform()].layerStore.Release(newL) var newImage image.Image newImage = *img @@ -313,7 +336,7 @@ func (daemon *Daemon) SquashImage(id, parent string) (string, error) { return "", errors.Wrap(err, "error marshalling image config") } - newImgID, err := daemon.imageStore.Create(b) + newImgID, err := daemon.stores[img.Platform()].imageStore.Create(b) if err != nil { return "", errors.Wrap(err, "error creating new image after squash") } diff --git a/daemon/import.go b/daemon/import.go index 0687533a0b..0409cd6bd6 100644 --- a/daemon/import.go +++ b/daemon/import.go @@ -26,13 +26,18 @@ import ( // inConfig (if src is "-"), or from a URI specified in src. Progress output is // written to outStream. Repository and tag names can optionally be given in // the repo and tag arguments, respectively. -func (daemon *Daemon) ImportImage(src string, repository, tag string, msg string, inConfig io.ReadCloser, outStream io.Writer, changes []string) error { +func (daemon *Daemon) ImportImage(src string, repository, platform string, tag string, msg string, inConfig io.ReadCloser, outStream io.Writer, changes []string) error { var ( rc io.ReadCloser resp *http.Response newRef reference.Named ) + // Default the platform if not supplied. + if platform == "" { + platform = runtime.GOOS + } + if repository != "" { var err error newRef, err = reference.ParseNormalizedNamed(repository) @@ -85,12 +90,11 @@ func (daemon *Daemon) ImportImage(src string, repository, tag string, msg string if err != nil { return err } - // TODO: support windows baselayer? - l, err := daemon.layerStore.Register(inflatedLayerData, "") + l, err := daemon.stores[platform].layerStore.Register(inflatedLayerData, "", layer.Platform(platform)) if err != nil { return err } - defer layer.ReleaseAndLog(daemon.layerStore, l) + defer layer.ReleaseAndLog(daemon.stores[platform].layerStore, l) created := time.Now().UTC() imgConfig, err := json.Marshal(&image.Image{ @@ -98,7 +102,7 @@ func (daemon *Daemon) ImportImage(src string, repository, tag string, msg string DockerVersion: dockerversion.Version, Config: config, Architecture: runtime.GOARCH, - OS: runtime.GOOS, + OS: platform, Created: created, Comment: msg, }, @@ -115,14 +119,14 @@ func (daemon *Daemon) ImportImage(src string, repository, tag string, msg string return err } - id, err := daemon.imageStore.Create(imgConfig) + id, err := daemon.stores[platform].imageStore.Create(imgConfig) if err != nil { return err } // FIXME: connect with commit code and call refstore directly if newRef != nil { - if err := daemon.TagImageWithReference(id, newRef); err != nil { + if err := daemon.TagImageWithReference(id, platform, newRef); err != nil { return err } } diff --git a/daemon/info.go b/daemon/info.go index 3c28cdf7f1..a11775c2e9 100644 --- a/daemon/info.go +++ b/daemon/info.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "runtime" + "strings" "time" "github.com/Sirupsen/logrus" @@ -77,15 +78,32 @@ func (daemon *Daemon) SystemInfo() (*types.Info, error) { securityOptions = append(securityOptions, "name=userns") } + imageCount := 0 + drivers := "" + for p, ds := range daemon.stores { + imageCount += len(ds.imageStore.Map()) + drivers += daemon.GraphDriverName(p) + if len(daemon.stores) > 1 { + drivers += fmt.Sprintf(" (%s) ", p) + } + } + + // TODO @jhowardmsft LCOW support. For now, hard-code the platform shown for the driver status + p := runtime.GOOS + if p == "windows" && system.LCOWSupported() { + p = "linux" + } + + drivers = strings.TrimSpace(drivers) v := &types.Info{ ID: daemon.ID, Containers: int(cRunning + cPaused + cStopped), ContainersRunning: int(cRunning), ContainersPaused: int(cPaused), ContainersStopped: int(cStopped), - Images: len(daemon.imageStore.Map()), - Driver: daemon.GraphDriverName(), - DriverStatus: daemon.layerStore.DriverStatus(), + Images: imageCount, + Driver: drivers, + DriverStatus: daemon.stores[p].layerStore.DriverStatus(), Plugins: daemon.showPluginsInfo(), IPv4Forwarding: !sysInfo.IPv4ForwardingDisabled, BridgeNfIptables: !sysInfo.BridgeNFCallIPTablesDisabled, diff --git a/daemon/inspect.go b/daemon/inspect.go index 4eb1d091d3..c981e7701d 100644 --- a/daemon/inspect.go +++ b/daemon/inspect.go @@ -170,6 +170,7 @@ func (daemon *Daemon) getInspectData(container *container.Container) (*types.Con Name: container.Name, RestartCount: container.RestartCount, Driver: container.Driver, + Platform: container.Platform, MountLabel: container.MountLabel, ProcessLabel: container.ProcessLabel, ExecIDs: container.GetExecIDs(), diff --git a/daemon/list.go b/daemon/list.go index 4d831460f2..e6909173e9 100644 --- a/daemon/list.go +++ b/daemon/list.go @@ -317,7 +317,7 @@ func (daemon *Daemon) foldFilter(config *types.ContainerListOptions) (*listConte if psFilters.Include("ancestor") { ancestorFilter = true psFilters.WalkValues("ancestor", func(ancestor string) error { - id, err := daemon.GetImageID(ancestor) + id, platform, err := daemon.GetImageIDAndPlatform(ancestor) if err != nil { logrus.Warnf("Error while looking up for image %v", ancestor) return nil @@ -327,7 +327,7 @@ func (daemon *Daemon) foldFilter(config *types.ContainerListOptions) (*listConte return nil } // Then walk down the graph and put the imageIds in imagesFilter - populateImageFilterByParents(imagesFilter, id, daemon.imageStore.Children) + populateImageFilterByParents(imagesFilter, id, daemon.stores[platform].imageStore.Children) return nil }) } @@ -558,7 +558,7 @@ func (daemon *Daemon) transformContainer(container *container.Container, ctx *li image := container.Config.Image // if possible keep the original ref if image != container.ImageID.String() { - id, err := daemon.GetImageID(image) + id, _, err := daemon.GetImageIDAndPlatform(image) if _, isDNE := err.(ErrImageDoesNotExist); err != nil && !isDNE { return nil, err } diff --git a/daemon/oci_windows.go b/daemon/oci_windows.go index d180518faa..67e3473537 100644 --- a/daemon/oci_windows.go +++ b/daemon/oci_windows.go @@ -7,11 +7,17 @@ import ( "github.com/docker/docker/container" "github.com/docker/docker/oci" "github.com/docker/docker/pkg/sysinfo" + "github.com/docker/docker/pkg/system" "github.com/opencontainers/runtime-spec/specs-go" ) func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) { - s := oci.DefaultSpec() + img, err := daemon.GetImage(string(c.ImageID)) + if err != nil { + return nil, err + } + + s := oci.DefaultOSSpec(img.OS) linkedEnv, err := daemon.setupLinkedContainers(c) if err != nil { @@ -95,7 +101,30 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) { if !c.Config.ArgsEscaped { s.Process.Args = escapeArgs(s.Process.Args) } + s.Process.Cwd = c.Config.WorkingDir + s.Process.Env = c.CreateDaemonEnvironment(c.Config.Tty, linkedEnv) + if c.Config.Tty { + s.Process.Terminal = c.Config.Tty + s.Process.ConsoleSize.Height = c.HostConfig.ConsoleSize[0] + s.Process.ConsoleSize.Width = c.HostConfig.ConsoleSize[1] + } + s.Process.User.Username = c.Config.User + + if img.OS == "windows" { + daemon.createSpecWindowsFields(c, &s, isHyperV) + } else { + // TODO @jhowardmsft LCOW Support. Modify this check when running in dual-mode + if system.LCOWSupported() && img.OS == "linux" { + daemon.createSpecLinuxFields(c, &s) + } + } + + return (*specs.Spec)(&s), nil +} + +// Sets the Windows-specific fields of the OCI spec +func (daemon *Daemon) createSpecWindowsFields(c *container.Container, s *specs.Spec, isHyperV bool) { if len(s.Process.Cwd) == 0 { // We default to C:\ to workaround the oddity of the case that the // default directory for cmd running as LocalSystem (or @@ -106,17 +135,11 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) { // as c:\. Hence, setting it to default of c:\ makes for consistency. s.Process.Cwd = `C:\` } - s.Process.Env = c.CreateDaemonEnvironment(c.Config.Tty, linkedEnv) - s.Process.ConsoleSize.Height = c.HostConfig.ConsoleSize[0] - s.Process.ConsoleSize.Width = c.HostConfig.ConsoleSize[1] - s.Process.Terminal = c.Config.Tty - s.Process.User.Username = c.Config.User - // In spec.Root. This is not set for Hyper-V containers - if !isHyperV { - s.Root.Path = c.BaseFS - } s.Root.Readonly = false // Windows does not support a read-only root filesystem + if !isHyperV { + s.Root.Path = c.BaseFS // This is not set for Hyper-V containers + } // In s.Windows.Resources cpuShares := uint16(c.HostConfig.CPUShares) @@ -157,7 +180,17 @@ func (daemon *Daemon) createSpec(c *container.Container) (*specs.Spec, error) { Iops: &c.HostConfig.IOMaximumIOps, }, } - return (*specs.Spec)(&s), nil +} + +// Sets the Linux-specific fields of the OCI spec +// TODO: @jhowardmsft LCOW Support. We need to do a lot more pulling in what can +// be pulled in from oci_linux.go. +func (daemon *Daemon) createSpecLinuxFields(c *container.Container, s *specs.Spec) { + if len(s.Process.Cwd) == 0 { + s.Process.Cwd = `/` + } + s.Root.Path = "rootfs" + s.Root.Readonly = c.HostConfig.ReadonlyRootfs } func escapeArgs(args []string) []string { diff --git a/daemon/prune.go b/daemon/prune.go index 602de618c9..561ac1e01e 100644 --- a/daemon/prune.go +++ b/daemon/prune.go @@ -3,6 +3,7 @@ package daemon import ( "fmt" "regexp" + "runtime" "sync/atomic" "time" @@ -14,6 +15,7 @@ import ( "github.com/docker/docker/image" "github.com/docker/docker/layer" "github.com/docker/docker/pkg/directory" + "github.com/docker/docker/pkg/system" "github.com/docker/docker/runconfig" "github.com/docker/docker/volume" "github.com/docker/libnetwork" @@ -157,6 +159,12 @@ func (daemon *Daemon) VolumesPrune(ctx context.Context, pruneFilters filters.Arg // ImagesPrune removes unused images func (daemon *Daemon) ImagesPrune(ctx context.Context, pruneFilters filters.Args) (*types.ImagesPruneReport, error) { + // TODO @jhowardmsft LCOW Support: This will need revisiting later. + platform := runtime.GOOS + if platform == "windows" && system.LCOWSupported() { + platform = "linux" + } + if !atomic.CompareAndSwapInt32(&daemon.pruneRunning, 0, 1) { return nil, errPruneRunning } @@ -186,9 +194,9 @@ func (daemon *Daemon) ImagesPrune(ctx context.Context, pruneFilters filters.Args var allImages map[image.ID]*image.Image if danglingOnly { - allImages = daemon.imageStore.Heads() + allImages = daemon.stores[platform].imageStore.Heads() } else { - allImages = daemon.imageStore.Map() + allImages = daemon.stores[platform].imageStore.Map() } allContainers := daemon.List() imageRefs := map[string]bool{} @@ -202,7 +210,7 @@ func (daemon *Daemon) ImagesPrune(ctx context.Context, pruneFilters filters.Args } // Filter intermediary images and get their unique size - allLayers := daemon.layerStore.Map() + allLayers := daemon.stores[platform].layerStore.Map() topImages := map[image.ID]*image.Image{} for id, img := range allImages { select { @@ -210,7 +218,7 @@ func (daemon *Daemon) ImagesPrune(ctx context.Context, pruneFilters filters.Args return nil, ctx.Err() default: dgst := digest.Digest(id) - if len(daemon.referenceStore.References(dgst)) == 0 && len(daemon.imageStore.Children(id)) != 0 { + if len(daemon.stores[platform].referenceStore.References(dgst)) == 0 && len(daemon.stores[platform].imageStore.Children(id)) != 0 { continue } if !until.IsZero() && img.Created.After(until) { @@ -241,7 +249,7 @@ deleteImagesLoop: } deletedImages := []types.ImageDeleteResponseItem{} - refs := daemon.referenceStore.References(dgst) + refs := daemon.stores[platform].referenceStore.References(dgst) if len(refs) > 0 { shouldDelete := !danglingOnly if !shouldDelete { diff --git a/daemon/start.go b/daemon/start.go index eddb5d3d50..61bc32f586 100644 --- a/daemon/start.go +++ b/daemon/start.go @@ -207,7 +207,7 @@ func (daemon *Daemon) Cleanup(container *container.Container) { if err := daemon.conditionalUnmountOnCleanup(container); err != nil { // FIXME: remove once reference counting for graphdrivers has been refactored // Ensure that all the mounts are gone - if mountid, err := daemon.layerStore.GetMountID(container.ID); err == nil { + if mountid, err := daemon.stores[container.Platform].layerStore.GetMountID(container.ID); err == nil { daemon.cleanupMountsByID(mountid) } } diff --git a/daemon/start_windows.go b/daemon/start_windows.go index 2578c5f330..da70a16112 100644 --- a/daemon/start_windows.go +++ b/daemon/start_windows.go @@ -41,7 +41,7 @@ func (daemon *Daemon) getLibcontainerdCreateOptions(container *container.Contain layerOpts.LayerFolderPath = m["dir"] // Generate the layer paths of the layer options - img, err := daemon.imageStore.Get(container.ImageID) + img, err := daemon.stores[container.Platform].imageStore.Get(container.ImageID) if err != nil { return nil, fmt.Errorf("failed to graph.Get on ImageID %s - %s", container.ImageID, err) } @@ -49,9 +49,9 @@ func (daemon *Daemon) getLibcontainerdCreateOptions(container *container.Contain max := len(img.RootFS.DiffIDs) for i := 1; i <= max; i++ { img.RootFS.DiffIDs = img.RootFS.DiffIDs[:i] - layerPath, err := layer.GetLayerPath(daemon.layerStore, img.RootFS.ChainID()) + layerPath, err := layer.GetLayerPath(daemon.stores[container.Platform].layerStore, img.RootFS.ChainID()) if err != nil { - return nil, fmt.Errorf("failed to get layer path from graphdriver %s for ImageID %s - %s", daemon.layerStore, img.RootFS.ChainID(), err) + return nil, fmt.Errorf("failed to get layer path from graphdriver %s for ImageID %s - %s", daemon.stores[container.Platform].layerStore, img.RootFS.ChainID(), err) } // Reverse order, expecting parent most first layerOpts.LayerPaths = append([]string{layerPath}, layerOpts.LayerPaths...) diff --git a/daemon/volumes_unix_test.go b/daemon/volumes_unix_test.go index 4be7719fe7..dcd6977eaa 100644 --- a/daemon/volumes_unix_test.go +++ b/daemon/volumes_unix_test.go @@ -18,70 +18,67 @@ func TestBackportMountSpec(t *testing.T) { d := Daemon{containers: container.NewMemoryStore()} c := &container.Container{ - CommonContainer: container.CommonContainer{ - State: &container.State{}, - MountPoints: map[string]*volume.MountPoint{ - "/apple": {Destination: "/apple", Source: "/var/lib/docker/volumes/12345678", Name: "12345678", RW: true, CopyData: true}, // anonymous volume - "/banana": {Destination: "/banana", Source: "/var/lib/docker/volumes/data", Name: "data", RW: true, CopyData: true}, // named volume - "/cherry": {Destination: "/cherry", Source: "/var/lib/docker/volumes/data", Name: "data", CopyData: true}, // RO named volume - "/dates": {Destination: "/dates", Source: "/var/lib/docker/volumes/data", Name: "data"}, // named volume nocopy - "/elderberry": {Destination: "/elderberry", Source: "/var/lib/docker/volumes/data", Name: "data"}, // masks anon vol - "/fig": {Destination: "/fig", Source: "/data", RW: true}, // RW bind - "/guava": {Destination: "/guava", Source: "/data", RW: false, Propagation: "shared"}, // RO bind + propagation - "/kumquat": {Destination: "/kumquat", Name: "data", RW: false, CopyData: true}, // volumes-from + State: &container.State{}, + MountPoints: map[string]*volume.MountPoint{ + "/apple": {Destination: "/apple", Source: "/var/lib/docker/volumes/12345678", Name: "12345678", RW: true, CopyData: true}, // anonymous volume + "/banana": {Destination: "/banana", Source: "/var/lib/docker/volumes/data", Name: "data", RW: true, CopyData: true}, // named volume + "/cherry": {Destination: "/cherry", Source: "/var/lib/docker/volumes/data", Name: "data", CopyData: true}, // RO named volume + "/dates": {Destination: "/dates", Source: "/var/lib/docker/volumes/data", Name: "data"}, // named volume nocopy + "/elderberry": {Destination: "/elderberry", Source: "/var/lib/docker/volumes/data", Name: "data"}, // masks anon vol + "/fig": {Destination: "/fig", Source: "/data", RW: true}, // RW bind + "/guava": {Destination: "/guava", Source: "/data", RW: false, Propagation: "shared"}, // RO bind + propagation + "/kumquat": {Destination: "/kumquat", Name: "data", RW: false, CopyData: true}, // volumes-from - // partially configured mountpoint due to #32613 - // specifically, `mp.Spec.Source` is not set - "/honeydew": { - Type: mounttypes.TypeVolume, - Destination: "/honeydew", - Name: "data", - Source: "/var/lib/docker/volumes/data", - Spec: mounttypes.Mount{Type: mounttypes.TypeVolume, Target: "/honeydew", VolumeOptions: &mounttypes.VolumeOptions{NoCopy: true}}, - }, + // partially configured mountpoint due to #32613 + // specifically, `mp.Spec.Source` is not set + "/honeydew": { + Type: mounttypes.TypeVolume, + Destination: "/honeydew", + Name: "data", + Source: "/var/lib/docker/volumes/data", + Spec: mounttypes.Mount{Type: mounttypes.TypeVolume, Target: "/honeydew", VolumeOptions: &mounttypes.VolumeOptions{NoCopy: true}}, + }, - // from hostconfig.Mounts - "/jambolan": { - Type: mounttypes.TypeVolume, - Destination: "/jambolan", - Source: "/var/lib/docker/volumes/data", - RW: true, - Name: "data", - Spec: mounttypes.Mount{Type: mounttypes.TypeVolume, Target: "/jambolan", Source: "data"}, - }, + // from hostconfig.Mounts + "/jambolan": { + Type: mounttypes.TypeVolume, + Destination: "/jambolan", + Source: "/var/lib/docker/volumes/data", + RW: true, + Name: "data", + Spec: mounttypes.Mount{Type: mounttypes.TypeVolume, Target: "/jambolan", Source: "data"}, }, - HostConfig: &containertypes.HostConfig{ - Binds: []string{ - "data:/banana", - "data:/cherry:ro", - "data:/dates:ro,nocopy", - "data:/elderberry:ro,nocopy", - "/data:/fig", - "/data:/guava:ro,shared", - "data:/honeydew:nocopy", - }, - VolumesFrom: []string{"1:ro"}, - Mounts: []mounttypes.Mount{ - {Type: mounttypes.TypeVolume, Target: "/jambolan"}, - }, + }, + HostConfig: &containertypes.HostConfig{ + Binds: []string{ + "data:/banana", + "data:/cherry:ro", + "data:/dates:ro,nocopy", + "data:/elderberry:ro,nocopy", + "/data:/fig", + "/data:/guava:ro,shared", + "data:/honeydew:nocopy", }, - Config: &containertypes.Config{Volumes: map[string]struct{}{ - "/apple": {}, - "/elderberry": {}, - }}, - }} + VolumesFrom: []string{"1:ro"}, + Mounts: []mounttypes.Mount{ + {Type: mounttypes.TypeVolume, Target: "/jambolan"}, + }, + }, + Config: &containertypes.Config{Volumes: map[string]struct{}{ + "/apple": {}, + "/elderberry": {}, + }}, + } d.containers.Add("1", &container.Container{ - CommonContainer: container.CommonContainer{ - State: &container.State{}, - ID: "1", - MountPoints: map[string]*volume.MountPoint{ - "/kumquat": {Destination: "/kumquat", Name: "data", RW: false, CopyData: true}, - }, - HostConfig: &containertypes.HostConfig{ - Binds: []string{ - "data:/kumquat:ro", - }, + State: &container.State{}, + ID: "1", + MountPoints: map[string]*volume.MountPoint{ + "/kumquat": {Destination: "/kumquat", Name: "data", RW: false, CopyData: true}, + }, + HostConfig: &containertypes.HostConfig{ + Binds: []string{ + "data:/kumquat:ro", }, }, }) diff --git a/distribution/config.go b/distribution/config.go index f24678d0e2..1c10533f69 100644 --- a/distribution/config.go +++ b/distribution/config.go @@ -14,6 +14,7 @@ import ( "github.com/docker/docker/image" "github.com/docker/docker/layer" "github.com/docker/docker/pkg/progress" + "github.com/docker/docker/pkg/system" refstore "github.com/docker/docker/reference" "github.com/docker/docker/registry" "github.com/docker/libtrust" @@ -58,6 +59,9 @@ type ImagePullConfig struct { // Schema2Types is the valid schema2 configuration types allowed // by the pull operation. Schema2Types []string + // Platform is the requested platform of the image being pulled to ensure it can be validated + // when the host platform supports multiple image operating systems. + Platform string } // ImagePushConfig stores push configuration. @@ -82,7 +86,7 @@ type ImagePushConfig struct { type ImageConfigStore interface { Put([]byte) (digest.Digest, error) Get(digest.Digest) ([]byte, error) - RootFSFromConfig([]byte) (*image.RootFS, error) + RootFSAndPlatformFromConfig([]byte) (*image.RootFS, layer.Platform, error) } // PushLayerProvider provides layers to be pushed by ChainID. @@ -108,7 +112,7 @@ type RootFSDownloadManager interface { // returns the final rootfs. // Given progress output to track download progress // Returns function to release download resources - Download(ctx context.Context, initialRootFS image.RootFS, layers []xfer.DownloadDescriptor, progressOutput progress.Output) (image.RootFS, func(), error) + Download(ctx context.Context, initialRootFS image.RootFS, platform layer.Platform, layers []xfer.DownloadDescriptor, progressOutput progress.Output) (image.RootFS, func(), error) } type imageConfigStore struct { @@ -136,21 +140,25 @@ func (s *imageConfigStore) Get(d digest.Digest) ([]byte, error) { return img.RawJSON(), nil } -func (s *imageConfigStore) RootFSFromConfig(c []byte) (*image.RootFS, error) { +func (s *imageConfigStore) RootFSAndPlatformFromConfig(c []byte) (*image.RootFS, layer.Platform, error) { var unmarshalledConfig image.Image if err := json.Unmarshal(c, &unmarshalledConfig); err != nil { - return nil, err + return nil, "", err } // fail immediately on Windows when downloading a non-Windows image - // and vice versa - if runtime.GOOS == "windows" && unmarshalledConfig.OS == "linux" { - return nil, fmt.Errorf("image operating system %q cannot be used on this platform", unmarshalledConfig.OS) + // and vice versa. Exception on Windows if Linux Containers are enabled. + if runtime.GOOS == "windows" && unmarshalledConfig.OS == "linux" && !system.LCOWSupported() { + return nil, "", fmt.Errorf("image operating system %q cannot be used on this platform", unmarshalledConfig.OS) } else if runtime.GOOS != "windows" && unmarshalledConfig.OS == "windows" { - return nil, fmt.Errorf("image operating system %q cannot be used on this platform", unmarshalledConfig.OS) + return nil, "", fmt.Errorf("image operating system %q cannot be used on this platform", unmarshalledConfig.OS) } - return unmarshalledConfig.RootFS, nil + platform := "" + if runtime.GOOS == "windows" { + platform = unmarshalledConfig.OS + } + return unmarshalledConfig.RootFS, layer.Platform(platform), nil } type storeLayerProvider struct { diff --git a/distribution/metadata/metadata.go b/distribution/metadata/metadata.go index 05ba4f817d..3dae79555d 100644 --- a/distribution/metadata/metadata.go +++ b/distribution/metadata/metadata.go @@ -26,15 +26,17 @@ type Store interface { type FSMetadataStore struct { sync.RWMutex basePath string + platform string } // NewFSMetadataStore creates a new filesystem-based metadata store. -func NewFSMetadataStore(basePath string) (*FSMetadataStore, error) { +func NewFSMetadataStore(basePath, platform string) (*FSMetadataStore, error) { if err := os.MkdirAll(basePath, 0700); err != nil { return nil, err } return &FSMetadataStore{ basePath: basePath, + platform: platform, }, nil } diff --git a/distribution/metadata/v1_id_service_test.go b/distribution/metadata/v1_id_service_test.go index 556886581e..385901ec46 100644 --- a/distribution/metadata/v1_id_service_test.go +++ b/distribution/metadata/v1_id_service_test.go @@ -3,6 +3,7 @@ package metadata import ( "io/ioutil" "os" + "runtime" "testing" "github.com/docker/docker/layer" @@ -15,7 +16,7 @@ func TestV1IDService(t *testing.T) { } defer os.RemoveAll(tmpDir) - metadataStore, err := NewFSMetadataStore(tmpDir) + metadataStore, err := NewFSMetadataStore(tmpDir, runtime.GOOS) if err != nil { t.Fatalf("could not create metadata store: %v", err) } diff --git a/distribution/metadata/v2_metadata_service_test.go b/distribution/metadata/v2_metadata_service_test.go index 8e3e4614c0..b5d59b2297 100644 --- a/distribution/metadata/v2_metadata_service_test.go +++ b/distribution/metadata/v2_metadata_service_test.go @@ -6,6 +6,7 @@ import ( "math/rand" "os" "reflect" + "runtime" "testing" "github.com/docker/docker/layer" @@ -19,7 +20,7 @@ func TestV2MetadataService(t *testing.T) { } defer os.RemoveAll(tmpDir) - metadataStore, err := NewFSMetadataStore(tmpDir) + metadataStore, err := NewFSMetadataStore(tmpDir, runtime.GOOS) if err != nil { t.Fatalf("could not create metadata store: %v", err) } diff --git a/distribution/pull_v1.go b/distribution/pull_v1.go index d873d338cd..7151a7584d 100644 --- a/distribution/pull_v1.go +++ b/distribution/pull_v1.go @@ -232,7 +232,7 @@ func (p *v1Puller) pullImage(ctx context.Context, v1ID, endpoint string, localNa } rootFS := image.NewRootFS() - resultRootFS, release, err := p.config.DownloadManager.Download(ctx, *rootFS, descriptors, p.config.ProgressOutput) + resultRootFS, release, err := p.config.DownloadManager.Download(ctx, *rootFS, "", descriptors, p.config.ProgressOutput) if err != nil { return err } diff --git a/distribution/pull_v2.go b/distribution/pull_v2.go index 08df6b768f..0f3f31cf52 100644 --- a/distribution/pull_v2.go +++ b/distribution/pull_v2.go @@ -27,6 +27,7 @@ import ( "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/system" refstore "github.com/docker/docker/reference" "github.com/docker/docker/registry" "github.com/opencontainers/go-digest" @@ -486,7 +487,26 @@ func (p *v2Puller) pullSchema1(ctx context.Context, ref reference.Named, unverif descriptors = append(descriptors, layerDescriptor) } - resultRootFS, release, err := p.config.DownloadManager.Download(ctx, *rootFS, descriptors, p.config.ProgressOutput) + // The v1 manifest itself doesn't directly contain a platform. However, + // the history does, but unfortunately that's a string, so search through + // all the history until hopefully we find one which indicates the os. + platform := runtime.GOOS + if runtime.GOOS == "windows" && system.LCOWSupported() { + type config struct { + Os string `json:"os,omitempty"` + } + for _, v := range verifiedManifest.History { + var c config + if err := json.Unmarshal([]byte(v.V1Compatibility), &c); err == nil { + if c.Os != "" { + platform = c.Os + break + } + } + } + } + + resultRootFS, release, err := p.config.DownloadManager.Download(ctx, *rootFS, layer.Platform(platform), descriptors, p.config.ProgressOutput) if err != nil { return "", "", err } @@ -556,10 +576,11 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s }() var ( - configJSON []byte // raw serialized image config - downloadedRootFS *image.RootFS // rootFS from registered layers - configRootFS *image.RootFS // rootFS from configuration - release func() // release resources from rootFS download + configJSON []byte // raw serialized image config + downloadedRootFS *image.RootFS // rootFS from registered layers + configRootFS *image.RootFS // rootFS from configuration + release func() // release resources from rootFS download + platform layer.Platform // for LCOW when registering downloaded layers ) // https://github.com/docker/docker/issues/24766 - Err on the side of caution, @@ -571,7 +592,7 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s // check to block Windows images being pulled on Linux is implemented, it // may be necessary to perform the same type of serialisation. if runtime.GOOS == "windows" { - configJSON, configRootFS, err = receiveConfig(p.config.ImageStore, configChan, configErrChan) + configJSON, configRootFS, platform, err = receiveConfig(p.config.ImageStore, configChan, configErrChan) if err != nil { return "", "", err } @@ -598,7 +619,7 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s rootFS image.RootFS ) downloadRootFS := *image.NewRootFS() - rootFS, release, err = p.config.DownloadManager.Download(ctx, downloadRootFS, descriptors, p.config.ProgressOutput) + rootFS, release, err = p.config.DownloadManager.Download(ctx, downloadRootFS, platform, descriptors, p.config.ProgressOutput) if err != nil { // Intentionally do not cancel the config download here // as the error from config download (if there is one) @@ -616,7 +637,7 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s } if configJSON == nil { - configJSON, configRootFS, err = receiveConfig(p.config.ImageStore, configChan, configErrChan) + configJSON, configRootFS, _, err = receiveConfig(p.config.ImageStore, configChan, configErrChan) if err == nil && configRootFS == nil { err = errRootFSInvalid } @@ -663,16 +684,16 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s return imageID, manifestDigest, nil } -func receiveConfig(s ImageConfigStore, configChan <-chan []byte, errChan <-chan error) ([]byte, *image.RootFS, error) { +func receiveConfig(s ImageConfigStore, configChan <-chan []byte, errChan <-chan error) ([]byte, *image.RootFS, layer.Platform, error) { select { case configJSON := <-configChan: - rootfs, err := s.RootFSFromConfig(configJSON) + rootfs, platform, err := s.RootFSAndPlatformFromConfig(configJSON) if err != nil { - return nil, nil, err + return nil, nil, "", err } - return configJSON, rootfs, nil + return configJSON, rootfs, platform, nil case err := <-errChan: - return nil, nil, err + return nil, nil, "", err // Don't need a case for ctx.Done in the select because cancellation // will trigger an error in p.pullSchema2ImageConfig. } diff --git a/distribution/push_v2.go b/distribution/push_v2.go index 5a6673780c..504083367e 100644 --- a/distribution/push_v2.go +++ b/distribution/push_v2.go @@ -118,7 +118,7 @@ func (p *v2Pusher) pushV2Tag(ctx context.Context, ref reference.NamedTagged, id return fmt.Errorf("could not find image from tag %s: %v", reference.FamiliarString(ref), err) } - rootfs, err := p.config.ImageStore.RootFSFromConfig(imgConfig) + rootfs, _, err := p.config.ImageStore.RootFSAndPlatformFromConfig(imgConfig) if err != nil { return fmt.Errorf("unable to get rootfs for image %s: %s", reference.FamiliarString(ref), err) } diff --git a/distribution/xfer/download.go b/distribution/xfer/download.go index 8bd48646d4..6769ee1cdf 100644 --- a/distribution/xfer/download.go +++ b/distribution/xfer/download.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "io" + "runtime" "time" "github.com/Sirupsen/logrus" @@ -22,7 +23,7 @@ const maxDownloadAttempts = 5 // registers and downloads those, taking into account dependencies between // layers. type LayerDownloadManager struct { - layerStore layer.Store + layerStores map[string]layer.Store tm TransferManager waitDuration time.Duration } @@ -33,9 +34,9 @@ func (ldm *LayerDownloadManager) SetConcurrency(concurrency int) { } // NewLayerDownloadManager returns a new LayerDownloadManager. -func NewLayerDownloadManager(layerStore layer.Store, concurrencyLimit int, options ...func(*LayerDownloadManager)) *LayerDownloadManager { +func NewLayerDownloadManager(layerStores map[string]layer.Store, concurrencyLimit int, options ...func(*LayerDownloadManager)) *LayerDownloadManager { manager := LayerDownloadManager{ - layerStore: layerStore, + layerStores: layerStores, tm: NewTransferManager(concurrencyLimit), waitDuration: time.Second, } @@ -94,7 +95,7 @@ type DownloadDescriptorWithRegistered interface { // Download method is called to get the layer tar data. Layers are then // registered in the appropriate order. The caller must call the returned // release function once it is done with the returned RootFS object. -func (ldm *LayerDownloadManager) Download(ctx context.Context, initialRootFS image.RootFS, layers []DownloadDescriptor, progressOutput progress.Output) (image.RootFS, func(), error) { +func (ldm *LayerDownloadManager) Download(ctx context.Context, initialRootFS image.RootFS, platform layer.Platform, layers []DownloadDescriptor, progressOutput progress.Output) (image.RootFS, func(), error) { var ( topLayer layer.Layer topDownload *downloadTransfer @@ -104,6 +105,11 @@ func (ldm *LayerDownloadManager) Download(ctx context.Context, initialRootFS ima downloadsByKey = make(map[string]*downloadTransfer) ) + // Assume that the platform is the host OS if blank + if platform == "" { + platform = layer.Platform(runtime.GOOS) + } + rootFS := initialRootFS for _, descriptor := range layers { key := descriptor.Key() @@ -115,13 +121,13 @@ func (ldm *LayerDownloadManager) Download(ctx context.Context, initialRootFS ima if err == nil { getRootFS := rootFS getRootFS.Append(diffID) - l, err := ldm.layerStore.Get(getRootFS.ChainID()) + l, err := ldm.layerStores[string(platform)].Get(getRootFS.ChainID()) if err == nil { // Layer already exists. logrus.Debugf("Layer already exists: %s", descriptor.ID()) progress.Update(progressOutput, descriptor.ID(), "Already exists") if topLayer != nil { - layer.ReleaseAndLog(ldm.layerStore, topLayer) + layer.ReleaseAndLog(ldm.layerStores[string(platform)], topLayer) } topLayer = l missingLayer = false @@ -140,7 +146,7 @@ func (ldm *LayerDownloadManager) Download(ctx context.Context, initialRootFS ima // the stack? If so, avoid downloading it more than once. var topDownloadUncasted Transfer if existingDownload, ok := downloadsByKey[key]; ok { - xferFunc := ldm.makeDownloadFuncFromDownload(descriptor, existingDownload, topDownload) + xferFunc := ldm.makeDownloadFuncFromDownload(descriptor, existingDownload, topDownload, platform) defer topDownload.Transfer.Release(watcher) topDownloadUncasted, watcher = ldm.tm.Transfer(transferKey, xferFunc, progressOutput) topDownload = topDownloadUncasted.(*downloadTransfer) @@ -152,10 +158,10 @@ func (ldm *LayerDownloadManager) Download(ctx context.Context, initialRootFS ima var xferFunc DoFunc if topDownload != nil { - xferFunc = ldm.makeDownloadFunc(descriptor, "", topDownload) + xferFunc = ldm.makeDownloadFunc(descriptor, "", topDownload, platform) defer topDownload.Transfer.Release(watcher) } else { - xferFunc = ldm.makeDownloadFunc(descriptor, rootFS.ChainID(), nil) + xferFunc = ldm.makeDownloadFunc(descriptor, rootFS.ChainID(), nil, platform) } topDownloadUncasted, watcher = ldm.tm.Transfer(transferKey, xferFunc, progressOutput) topDownload = topDownloadUncasted.(*downloadTransfer) @@ -165,7 +171,7 @@ func (ldm *LayerDownloadManager) Download(ctx context.Context, initialRootFS ima if topDownload == nil { return rootFS, func() { if topLayer != nil { - layer.ReleaseAndLog(ldm.layerStore, topLayer) + layer.ReleaseAndLog(ldm.layerStores[string(platform)], topLayer) } }, nil } @@ -176,7 +182,7 @@ func (ldm *LayerDownloadManager) Download(ctx context.Context, initialRootFS ima defer func() { if topLayer != nil { - layer.ReleaseAndLog(ldm.layerStore, topLayer) + layer.ReleaseAndLog(ldm.layerStores[string(platform)], topLayer) } }() @@ -212,11 +218,11 @@ func (ldm *LayerDownloadManager) Download(ctx context.Context, initialRootFS ima // complete before the registration step, and registers the downloaded data // on top of parentDownload's resulting layer. Otherwise, it registers the // layer on top of the ChainID given by parentLayer. -func (ldm *LayerDownloadManager) makeDownloadFunc(descriptor DownloadDescriptor, parentLayer layer.ChainID, parentDownload *downloadTransfer) DoFunc { +func (ldm *LayerDownloadManager) makeDownloadFunc(descriptor DownloadDescriptor, parentLayer layer.ChainID, parentDownload *downloadTransfer, platform layer.Platform) DoFunc { return func(progressChan chan<- progress.Progress, start <-chan struct{}, inactive chan<- struct{}) Transfer { d := &downloadTransfer{ Transfer: NewTransfer(), - layerStore: ldm.layerStore, + layerStore: ldm.layerStores[string(platform)], } go func() { @@ -335,9 +341,9 @@ func (ldm *LayerDownloadManager) makeDownloadFunc(descriptor DownloadDescriptor, src = fs.Descriptor() } if ds, ok := d.layerStore.(layer.DescribableStore); ok { - d.layer, err = ds.RegisterWithDescriptor(inflatedLayerData, parentLayer, src) + d.layer, err = ds.RegisterWithDescriptor(inflatedLayerData, parentLayer, platform, src) } else { - d.layer, err = d.layerStore.Register(inflatedLayerData, parentLayer) + d.layer, err = d.layerStore.Register(inflatedLayerData, parentLayer, platform) } if err != nil { select { @@ -376,11 +382,11 @@ func (ldm *LayerDownloadManager) makeDownloadFunc(descriptor DownloadDescriptor, // parentDownload. This function does not log progress output because it would // interfere with the progress reporting for sourceDownload, which has the same // Key. -func (ldm *LayerDownloadManager) makeDownloadFuncFromDownload(descriptor DownloadDescriptor, sourceDownload *downloadTransfer, parentDownload *downloadTransfer) DoFunc { +func (ldm *LayerDownloadManager) makeDownloadFuncFromDownload(descriptor DownloadDescriptor, sourceDownload *downloadTransfer, parentDownload *downloadTransfer, platform layer.Platform) DoFunc { return func(progressChan chan<- progress.Progress, start <-chan struct{}, inactive chan<- struct{}) Transfer { d := &downloadTransfer{ Transfer: NewTransfer(), - layerStore: ldm.layerStore, + layerStore: ldm.layerStores[string(platform)], } go func() { @@ -434,9 +440,9 @@ func (ldm *LayerDownloadManager) makeDownloadFuncFromDownload(descriptor Downloa src = fs.Descriptor() } if ds, ok := d.layerStore.(layer.DescribableStore); ok { - d.layer, err = ds.RegisterWithDescriptor(layerReader, parentLayer, src) + d.layer, err = ds.RegisterWithDescriptor(layerReader, parentLayer, platform, src) } else { - d.layer, err = d.layerStore.Register(layerReader, parentLayer) + d.layer, err = d.layerStore.Register(layerReader, parentLayer, platform) } if err != nil { d.err = fmt.Errorf("failed to register layer: %v", err) diff --git a/distribution/xfer/download_test.go b/distribution/xfer/download_test.go index 69323bb869..e5aba02e3b 100644 --- a/distribution/xfer/download_test.go +++ b/distribution/xfer/download_test.go @@ -26,6 +26,7 @@ type mockLayer struct { diffID layer.DiffID chainID layer.ChainID parent layer.Layer + platform layer.Platform } func (ml *mockLayer) TarStream() (io.ReadCloser, error) { @@ -56,6 +57,10 @@ func (ml *mockLayer) DiffSize() (size int64, err error) { return 0, nil } +func (ml *mockLayer) Platform() layer.Platform { + return ml.platform +} + func (ml *mockLayer) Metadata() (map[string]string, error) { return make(map[string]string), nil } @@ -86,7 +91,7 @@ func (ls *mockLayerStore) Map() map[layer.ChainID]layer.Layer { return layers } -func (ls *mockLayerStore) Register(reader io.Reader, parentID layer.ChainID) (layer.Layer, error) { +func (ls *mockLayerStore) Register(reader io.Reader, parentID layer.ChainID, platform layer.Platform) (layer.Layer, error) { return ls.RegisterWithDescriptor(reader, parentID, distribution.Descriptor{}) } @@ -267,7 +272,9 @@ func TestSuccessfulDownload(t *testing.T) { } layerStore := &mockLayerStore{make(map[layer.ChainID]*mockLayer)} - ldm := NewLayerDownloadManager(layerStore, maxDownloadConcurrency, func(m *LayerDownloadManager) { m.waitDuration = time.Millisecond }) + lsMap := make(map[string]layer.Store) + lsMap[runtime.GOOS] = layerStore + ldm := NewLayerDownloadManager(lsMap, maxDownloadConcurrency, func(m *LayerDownloadManager) { m.waitDuration = time.Millisecond }) progressChan := make(chan progress.Progress) progressDone := make(chan struct{}) @@ -286,13 +293,13 @@ func TestSuccessfulDownload(t *testing.T) { firstDescriptor := descriptors[0].(*mockDownloadDescriptor) // Pre-register the first layer to simulate an already-existing layer - l, err := layerStore.Register(firstDescriptor.mockTarStream(), "") + l, err := layerStore.Register(firstDescriptor.mockTarStream(), "", layer.Platform(runtime.GOOS)) if err != nil { t.Fatal(err) } firstDescriptor.diffID = l.DiffID() - rootFS, releaseFunc, err := ldm.Download(context.Background(), *image.NewRootFS(), descriptors, progress.ChanOutput(progressChan)) + rootFS, releaseFunc, err := ldm.Download(context.Background(), *image.NewRootFS(), layer.Platform(runtime.GOOS), descriptors, progress.ChanOutput(progressChan)) if err != nil { t.Fatalf("download error: %v", err) } @@ -328,7 +335,10 @@ func TestSuccessfulDownload(t *testing.T) { } func TestCancelledDownload(t *testing.T) { - ldm := NewLayerDownloadManager(&mockLayerStore{make(map[layer.ChainID]*mockLayer)}, maxDownloadConcurrency, func(m *LayerDownloadManager) { m.waitDuration = time.Millisecond }) + layerStore := &mockLayerStore{make(map[layer.ChainID]*mockLayer)} + lsMap := make(map[string]layer.Store) + lsMap[runtime.GOOS] = layerStore + ldm := NewLayerDownloadManager(lsMap, maxDownloadConcurrency, func(m *LayerDownloadManager) { m.waitDuration = time.Millisecond }) progressChan := make(chan progress.Progress) progressDone := make(chan struct{}) @@ -347,7 +357,7 @@ func TestCancelledDownload(t *testing.T) { }() descriptors := downloadDescriptors(nil) - _, _, err := ldm.Download(ctx, *image.NewRootFS(), descriptors, progress.ChanOutput(progressChan)) + _, _, err := ldm.Download(ctx, *image.NewRootFS(), layer.Platform(runtime.GOOS), descriptors, progress.ChanOutput(progressChan)) if err != context.Canceled { t.Fatal("expected download to be cancelled") } diff --git a/image/image.go b/image/image.go index 9f892d298a..ab95d93dab 100644 --- a/image/image.go +++ b/image/image.go @@ -96,6 +96,15 @@ func (img *Image) RunConfig() *container.Config { return img.Config } +// Platform returns the image's operating system. If not populated, defaults to the host runtime OS. +func (img *Image) Platform() string { + os := img.OS + if os == "" { + os = runtime.GOOS + } + return os +} + // MarshalJSON serializes the image to JSON. It sorts the top-level keys so // that JSON that's been manipulated by a push/pull cycle with a legacy // registry won't end up with a different key order. @@ -126,7 +135,7 @@ type ChildConfig struct { } // NewChildImage creates a new Image as a child of this image. -func NewChildImage(img *Image, child ChildConfig) *Image { +func NewChildImage(img *Image, child ChildConfig, platform string) *Image { isEmptyLayer := layer.IsEmpty(child.DiffID) rootFS := img.RootFS if rootFS == nil { @@ -146,7 +155,7 @@ func NewChildImage(img *Image, child ChildConfig) *Image { DockerVersion: dockerversion.Version, Config: child.Config, Architecture: runtime.GOARCH, - OS: runtime.GOOS, + OS: platform, Container: child.ContainerID, ContainerConfig: *child.ContainerConfig, Author: child.Author, diff --git a/image/store.go b/image/store.go index dbcc30f481..541b07bba1 100644 --- a/image/store.go +++ b/image/store.go @@ -3,11 +3,14 @@ package image import ( "encoding/json" "fmt" + "runtime" + "strings" "sync" "github.com/Sirupsen/logrus" "github.com/docker/distribution/digestset" "github.com/docker/docker/layer" + "github.com/docker/docker/pkg/system" "github.com/opencontainers/go-digest" "github.com/pkg/errors" ) @@ -42,15 +45,17 @@ type store struct { images map[ID]*imageMeta fs StoreBackend digestSet *digestset.Set + platform string } // NewImageStore returns new store object for given layer store -func NewImageStore(fs StoreBackend, ls LayerGetReleaser) (Store, error) { +func NewImageStore(fs StoreBackend, platform string, ls LayerGetReleaser) (Store, error) { is := &store{ ls: ls, images: make(map[ID]*imageMeta), fs: fs, digestSet: digestset.NewSet(), + platform: platform, } // load all current images and retain layers @@ -111,6 +116,13 @@ func (is *store) Create(config []byte) (ID, error) { return "", err } + // Integrity check - ensure we are creating something for the correct platform + if runtime.GOOS == "windows" && system.LCOWSupported() { + if strings.ToLower(img.Platform()) != strings.ToLower(is.platform) { + return "", fmt.Errorf("cannot create entry for platform %q in image store for platform %q", img.Platform(), is.platform) + } + } + // Must reject any config that references diffIDs from the history // which aren't among the rootfs layers. rootFSLayers := make(map[layer.DiffID]struct{}) diff --git a/image/store_test.go b/image/store_test.go index 21a9b0b41b..13318cf221 100644 --- a/image/store_test.go +++ b/image/store_test.go @@ -1,6 +1,7 @@ package image import ( + "runtime" "testing" "github.com/docker/docker/layer" @@ -25,7 +26,7 @@ func TestRestore(t *testing.T) { err = fs.SetMetadata(id2, "parent", []byte(id1)) assert.NoError(t, err) - is, err := NewImageStore(fs, &mockLayerGetReleaser{}) + is, err := NewImageStore(fs, runtime.GOOS, &mockLayerGetReleaser{}) assert.NoError(t, err) assert.Len(t, is.Map(), 2) @@ -142,7 +143,7 @@ func TestParentReset(t *testing.T) { func defaultImageStore(t *testing.T) (Store, func()) { fsBackend, cleanup := defaultFSStoreBackend(t) - store, err := NewImageStore(fsBackend, &mockLayerGetReleaser{}) + store, err := NewImageStore(fsBackend, runtime.GOOS, &mockLayerGetReleaser{}) assert.NoError(t, err) return store, cleanup diff --git a/image/tarexport/load.go b/image/tarexport/load.go index 14b7c1af0b..adf4574da8 100644 --- a/image/tarexport/load.go +++ b/image/tarexport/load.go @@ -2,12 +2,14 @@ package tarexport import ( "encoding/json" + "errors" "fmt" "io" "io/ioutil" "os" "path/filepath" "reflect" + "runtime" "github.com/Sirupsen/logrus" "github.com/docker/distribution" @@ -85,6 +87,17 @@ func (l *tarexporter) Load(inTar io.ReadCloser, outStream io.Writer, quiet bool) return fmt.Errorf("invalid manifest, layers length mismatch: expected %d, got %d", expected, actual) } + // On Windows, validate the platform, defaulting to windows if not present. + platform := layer.Platform(img.OS) + if runtime.GOOS == "windows" { + if platform == "" { + platform = "windows" + } + if (platform != "windows") && (platform != "linux") { + return fmt.Errorf("configuration for this image has an unsupported platform: %s", platform) + } + } + for i, diffID := range img.RootFS.DiffIDs { layerPath, err := safePath(tmpDir, m.Layers[i]) if err != nil { @@ -94,7 +107,7 @@ func (l *tarexporter) Load(inTar io.ReadCloser, outStream io.Writer, quiet bool) r.Append(diffID) newLayer, err := l.ls.Get(r.ChainID()) if err != nil { - newLayer, err = l.loadLayer(layerPath, rootFS, diffID.String(), m.LayerSources[diffID], progressOutput) + newLayer, err = l.loadLayer(layerPath, rootFS, diffID.String(), platform, m.LayerSources[diffID], progressOutput) if err != nil { return err } @@ -161,7 +174,7 @@ func (l *tarexporter) setParentID(id, parentID image.ID) error { return l.is.SetParent(id, parentID) } -func (l *tarexporter) loadLayer(filename string, rootFS image.RootFS, id string, foreignSrc distribution.Descriptor, progressOutput progress.Output) (layer.Layer, error) { +func (l *tarexporter) loadLayer(filename string, rootFS image.RootFS, id string, platform layer.Platform, foreignSrc distribution.Descriptor, progressOutput progress.Output) (layer.Layer, error) { // We use system.OpenSequential to use sequential file access on Windows, avoiding // depleting the standby list. On Linux, this equates to a regular os.Open. rawTar, err := system.OpenSequential(filename) @@ -191,9 +204,9 @@ func (l *tarexporter) loadLayer(filename string, rootFS image.RootFS, id string, defer inflatedLayerData.Close() if ds, ok := l.ls.(layer.DescribableStore); ok { - return ds.RegisterWithDescriptor(inflatedLayerData, rootFS.ChainID(), foreignSrc) + return ds.RegisterWithDescriptor(inflatedLayerData, rootFS.ChainID(), platform, foreignSrc) } - return l.ls.Register(inflatedLayerData, rootFS.ChainID()) + return l.ls.Register(inflatedLayerData, rootFS.ChainID(), platform) } func (l *tarexporter) setLoadedTag(ref reference.NamedTagged, imgID digest.Digest, outStream io.Writer) error { @@ -208,6 +221,10 @@ func (l *tarexporter) setLoadedTag(ref reference.NamedTagged, imgID digest.Diges } func (l *tarexporter) legacyLoad(tmpDir string, outStream io.Writer, progressOutput progress.Output) error { + if runtime.GOOS == "windows" { + return errors.New("Windows does not support legacy loading of images") + } + legacyLoadedMap := make(map[string]image.ID) dirs, err := ioutil.ReadDir(tmpDir) @@ -312,7 +329,7 @@ func (l *tarexporter) legacyLoadImage(oldID, sourceDir string, loadedMap map[str if err != nil { return err } - newLayer, err := l.loadLayer(layerPath, *rootFS, oldID, distribution.Descriptor{}, progressOutput) + newLayer, err := l.loadLayer(layerPath, *rootFS, oldID, "", distribution.Descriptor{}, progressOutput) if err != nil { return err } diff --git a/layer/empty.go b/layer/empty.go index 80f2c12439..cf04aa12fe 100644 --- a/layer/empty.go +++ b/layer/empty.go @@ -55,6 +55,10 @@ func (el *emptyLayer) Metadata() (map[string]string, error) { return make(map[string]string), nil } +func (el *emptyLayer) Platform() Platform { + return "" +} + // IsEmpty returns true if the layer is an EmptyLayer func IsEmpty(diffID DiffID) bool { return diffID == DigestSHA256EmptyTar diff --git a/layer/filestore_unix.go b/layer/filestore_unix.go new file mode 100644 index 0000000000..fe8a4f8b2a --- /dev/null +++ b/layer/filestore_unix.go @@ -0,0 +1,13 @@ +// +build !windows + +package layer + +// SetPlatform writes the "platform" file to the layer filestore +func (fm *fileMetadataTransaction) SetPlatform(platform Platform) error { + return nil +} + +// GetPlatform reads the "platform" file from the layer filestore +func (fms *fileMetadataStore) GetPlatform(layer ChainID) (Platform, error) { + return "", nil +} diff --git a/layer/filestore_windows.go b/layer/filestore_windows.go new file mode 100644 index 0000000000..066456d8d2 --- /dev/null +++ b/layer/filestore_windows.go @@ -0,0 +1,35 @@ +package layer + +import ( + "fmt" + "io/ioutil" + "os" + "strings" +) + +// SetPlatform writes the "platform" file to the layer filestore +func (fm *fileMetadataTransaction) SetPlatform(platform Platform) error { + if platform == "" { + return nil + } + return fm.ws.WriteFile("platform", []byte(platform), 0644) +} + +// GetPlatform reads the "platform" file from the layer filestore +func (fms *fileMetadataStore) GetPlatform(layer ChainID) (Platform, error) { + contentBytes, err := ioutil.ReadFile(fms.getLayerFilename(layer, "platform")) + if err != nil { + // For backwards compatibility, the platform file may not exist. Default to "windows" if missing. + if os.IsNotExist(err) { + return "windows", nil + } + return "", err + } + content := strings.TrimSpace(string(contentBytes)) + + if content != "windows" && content != "linux" { + return "", fmt.Errorf("invalid platform value: %s", content) + } + + return Platform(content), nil +} diff --git a/layer/layer.go b/layer/layer.go index 7b993ee4ad..b3480a0cc1 100644 --- a/layer/layer.go +++ b/layer/layer.go @@ -64,6 +64,14 @@ func (id ChainID) String() string { return string(id) } +// Platform is the platform of a layer +type Platform string + +// String returns a string rendition of layers target platform +func (id Platform) String() string { + return string(id) +} + // DiffID is the hash of an individual layer tar. type DiffID digest.Digest @@ -99,6 +107,9 @@ type Layer interface { // Parent returns the next layer in the layer chain. Parent() Layer + // Platform returns the platform of the layer + Platform() Platform + // Size returns the size of the entire layer chain. The size // is calculated from the total size of all files in the layers. Size() (int64, error) @@ -179,7 +190,7 @@ type CreateRWLayerOpts struct { // Store represents a backend for managing both // read-only and read-write layers. type Store interface { - Register(io.Reader, ChainID) (Layer, error) + Register(io.Reader, ChainID, Platform) (Layer, error) Get(ChainID) (Layer, error) Map() map[ChainID]Layer Release(Layer) ([]Metadata, error) @@ -197,7 +208,7 @@ type Store interface { // DescribableStore represents a layer store capable of storing // descriptors for layers. type DescribableStore interface { - RegisterWithDescriptor(io.Reader, ChainID, distribution.Descriptor) (Layer, error) + RegisterWithDescriptor(io.Reader, ChainID, Platform, distribution.Descriptor) (Layer, error) } // MetadataTransaction represents functions for setting layer metadata @@ -208,6 +219,7 @@ type MetadataTransaction interface { SetDiffID(DiffID) error SetCacheID(string) error SetDescriptor(distribution.Descriptor) error + SetPlatform(Platform) error TarSplitWriter(compressInput bool) (io.WriteCloser, error) Commit(ChainID) error @@ -228,6 +240,7 @@ type MetadataStore interface { GetDiffID(ChainID) (DiffID, error) GetCacheID(ChainID) (string, error) GetDescriptor(ChainID) (distribution.Descriptor, error) + GetPlatform(ChainID) (Platform, error) TarSplitReader(ChainID) (io.ReadCloser, error) SetMountID(string, string) error diff --git a/layer/layer_store.go b/layer/layer_store.go index 25861c6669..e7c424aa92 100644 --- a/layer/layer_store.go +++ b/layer/layer_store.go @@ -5,6 +5,8 @@ import ( "fmt" "io" "io/ioutil" + "runtime" + "strings" "sync" "github.com/Sirupsen/logrus" @@ -13,6 +15,7 @@ import ( "github.com/docker/docker/pkg/idtools" "github.com/docker/docker/pkg/plugingetter" "github.com/docker/docker/pkg/stringid" + "github.com/docker/docker/pkg/system" "github.com/opencontainers/go-digest" "github.com/vbatts/tar-split/tar/asm" "github.com/vbatts/tar-split/tar/storage" @@ -36,6 +39,8 @@ type layerStore struct { mountL sync.Mutex useTarSplit bool + + platform string } // StoreOptions are the options used to create a new Store instance @@ -47,6 +52,7 @@ type StoreOptions struct { IDMappings *idtools.IDMappings PluginGetter plugingetter.PluginGetter ExperimentalEnabled bool + Platform string } // NewStoreFromOptions creates a new Store instance @@ -68,13 +74,13 @@ func NewStoreFromOptions(options StoreOptions) (Store, error) { return nil, err } - return NewStoreFromGraphDriver(fms, driver) + return NewStoreFromGraphDriver(fms, driver, options.Platform) } // NewStoreFromGraphDriver creates a new Store instance using the provided // metadata store and graph driver. The metadata store will be used to restore // the Store. -func NewStoreFromGraphDriver(store MetadataStore, driver graphdriver.Driver) (Store, error) { +func NewStoreFromGraphDriver(store MetadataStore, driver graphdriver.Driver, platform string) (Store, error) { caps := graphdriver.Capabilities{} if capDriver, ok := driver.(graphdriver.CapabilityDriver); ok { caps = capDriver.Capabilities() @@ -86,6 +92,7 @@ func NewStoreFromGraphDriver(store MetadataStore, driver graphdriver.Driver) (St layerMap: map[ChainID]*roLayer{}, mounts: map[string]*mountedLayer{}, useTarSplit: !caps.ReproducesExactDiffs, + platform: platform, } ids, mounts, err := store.List() @@ -144,6 +151,11 @@ func (ls *layerStore) loadLayer(layer ChainID) (*roLayer, error) { return nil, fmt.Errorf("failed to get descriptor for %s: %s", layer, err) } + platform, err := ls.store.GetPlatform(layer) + if err != nil { + return nil, fmt.Errorf("failed to get platform for %s: %s", layer, err) + } + cl = &roLayer{ chainID: layer, diffID: diff, @@ -152,6 +164,7 @@ func (ls *layerStore) loadLayer(layer ChainID) (*roLayer, error) { layerStore: ls, references: map[Layer]struct{}{}, descriptor: descriptor, + platform: platform, } if parent != "" { @@ -247,17 +260,25 @@ func (ls *layerStore) applyTar(tx MetadataTransaction, ts io.Reader, parent stri return nil } -func (ls *layerStore) Register(ts io.Reader, parent ChainID) (Layer, error) { - return ls.registerWithDescriptor(ts, parent, distribution.Descriptor{}) +func (ls *layerStore) Register(ts io.Reader, parent ChainID, platform Platform) (Layer, error) { + return ls.registerWithDescriptor(ts, parent, platform, distribution.Descriptor{}) } -func (ls *layerStore) registerWithDescriptor(ts io.Reader, parent ChainID, descriptor distribution.Descriptor) (Layer, error) { +func (ls *layerStore) registerWithDescriptor(ts io.Reader, parent ChainID, platform Platform, descriptor distribution.Descriptor) (Layer, error) { // err is used to hold the error which will always trigger // cleanup of creates sources but may not be an error returned // to the caller (already exists). var err error var pid string var p *roLayer + + // Integrity check - ensure we are creating something for the correct platform + if runtime.GOOS == "windows" && system.LCOWSupported() { + if strings.ToLower(ls.platform) != strings.ToLower(string(platform)) { + return nil, fmt.Errorf("cannot create entry for platform %q in layer store for platform %q", platform, ls.platform) + } + } + if string(parent) != "" { p = ls.get(parent) if p == nil { @@ -286,6 +307,7 @@ func (ls *layerStore) registerWithDescriptor(ts io.Reader, parent ChainID, descr layerStore: ls, references: map[Layer]struct{}{}, descriptor: descriptor, + platform: platform, } if err = ls.driver.Create(layer.cacheID, pid, nil); err != nil { @@ -388,7 +410,6 @@ func (ls *layerStore) deleteLayer(layer *roLayer, metadata *Metadata) error { if err != nil { return err } - err = ls.store.Remove(layer.chainID) if err != nil { return err @@ -518,7 +539,6 @@ func (ls *layerStore) CreateRWLayer(name string, parent ChainID, opts *CreateRWL if err = ls.driver.CreateReadWrite(m.mountID, pid, createOpts); err != nil { return nil, err } - if err = ls.saveMount(m); err != nil { return nil, err } diff --git a/layer/layer_store_windows.go b/layer/layer_store_windows.go index 1276a912cc..ccbf6dd52a 100644 --- a/layer/layer_store_windows.go +++ b/layer/layer_store_windows.go @@ -6,6 +6,6 @@ import ( "github.com/docker/distribution" ) -func (ls *layerStore) RegisterWithDescriptor(ts io.Reader, parent ChainID, descriptor distribution.Descriptor) (Layer, error) { - return ls.registerWithDescriptor(ts, parent, descriptor) +func (ls *layerStore) RegisterWithDescriptor(ts io.Reader, parent ChainID, platform Platform, descriptor distribution.Descriptor) (Layer, error) { + return ls.registerWithDescriptor(ts, parent, platform, descriptor) } diff --git a/layer/layer_test.go b/layer/layer_test.go index 56340d3012..8ec5b4df54 100644 --- a/layer/layer_test.go +++ b/layer/layer_test.go @@ -71,7 +71,7 @@ func newTestStore(t *testing.T) (Store, string, func()) { if err != nil { t.Fatal(err) } - ls, err := NewStoreFromGraphDriver(fms, graph) + ls, err := NewStoreFromGraphDriver(fms, graph, runtime.GOOS) if err != nil { t.Fatal(err) } @@ -106,7 +106,7 @@ func createLayer(ls Store, parent ChainID, layerFunc layerInit) (Layer, error) { } defer ts.Close() - layer, err := ls.Register(ts, parent) + layer, err := ls.Register(ts, parent, Platform(runtime.GOOS)) if err != nil { return nil, err } @@ -404,7 +404,7 @@ func TestStoreRestore(t *testing.T) { t.Fatal(err) } - ls2, err := NewStoreFromGraphDriver(ls.(*layerStore).store, ls.(*layerStore).driver) + ls2, err := NewStoreFromGraphDriver(ls.(*layerStore).store, ls.(*layerStore).driver, runtime.GOOS) if err != nil { t.Fatal(err) } @@ -499,7 +499,7 @@ func TestTarStreamStability(t *testing.T) { t.Fatal(err) } - layer1, err := ls.Register(bytes.NewReader(tar1), "") + layer1, err := ls.Register(bytes.NewReader(tar1), "", Platform(runtime.GOOS)) if err != nil { t.Fatal(err) } @@ -518,7 +518,7 @@ func TestTarStreamStability(t *testing.T) { t.Fatal(err) } - layer2, err := ls.Register(bytes.NewReader(tar2), layer1.ChainID()) + layer2, err := ls.Register(bytes.NewReader(tar2), layer1.ChainID(), Platform(runtime.GOOS)) if err != nil { t.Fatal(err) } @@ -686,12 +686,12 @@ func TestRegisterExistingLayer(t *testing.T) { t.Fatal(err) } - layer2a, err := ls.Register(bytes.NewReader(tar1), layer1.ChainID()) + layer2a, err := ls.Register(bytes.NewReader(tar1), layer1.ChainID(), Platform(runtime.GOOS)) if err != nil { t.Fatal(err) } - layer2b, err := ls.Register(bytes.NewReader(tar1), layer1.ChainID()) + layer2b, err := ls.Register(bytes.NewReader(tar1), layer1.ChainID(), Platform(runtime.GOOS)) if err != nil { t.Fatal(err) } @@ -726,12 +726,12 @@ func TestTarStreamVerification(t *testing.T) { t.Fatal(err) } - layer1, err := ls.Register(bytes.NewReader(tar1), "") + layer1, err := ls.Register(bytes.NewReader(tar1), "", Platform(runtime.GOOS)) if err != nil { t.Fatal(err) } - layer2, err := ls.Register(bytes.NewReader(tar2), "") + layer2, err := ls.Register(bytes.NewReader(tar2), "", Platform(runtime.GOOS)) if err != nil { t.Fatal(err) } diff --git a/layer/migration_test.go b/layer/migration_test.go index fe3b40c20b..7364e6cdc8 100644 --- a/layer/migration_test.go +++ b/layer/migration_test.go @@ -94,7 +94,7 @@ func TestLayerMigration(t *testing.T) { if err != nil { t.Fatal(err) } - ls, err := NewStoreFromGraphDriver(fms, graph) + ls, err := NewStoreFromGraphDriver(fms, graph, runtime.GOOS) if err != nil { t.Fatal(err) } @@ -110,14 +110,14 @@ func TestLayerMigration(t *testing.T) { t.Fatal(err) } - layer1b, err := ls.Register(bytes.NewReader(tar1), "") + layer1b, err := ls.Register(bytes.NewReader(tar1), "", Platform(runtime.GOOS)) if err != nil { t.Fatal(err) } assertReferences(t, layer1a, layer1b) // Attempt register, should be same - layer2a, err := ls.Register(bytes.NewReader(tar2), layer1a.ChainID()) + layer2a, err := ls.Register(bytes.NewReader(tar2), layer1a.ChainID(), Platform(runtime.GOOS)) if err != nil { t.Fatal(err) } @@ -222,7 +222,7 @@ func TestLayerMigrationNoTarsplit(t *testing.T) { if err != nil { t.Fatal(err) } - ls, err := NewStoreFromGraphDriver(fms, graph) + ls, err := NewStoreFromGraphDriver(fms, graph, runtime.GOOS) if err != nil { t.Fatal(err) } @@ -238,7 +238,7 @@ func TestLayerMigrationNoTarsplit(t *testing.T) { t.Fatal(err) } - layer1b, err := ls.Register(bytes.NewReader(tar1), "") + layer1b, err := ls.Register(bytes.NewReader(tar1), "", Platform(runtime.GOOS)) if err != nil { t.Fatal(err) } @@ -246,7 +246,7 @@ func TestLayerMigrationNoTarsplit(t *testing.T) { assertReferences(t, layer1a, layer1b) // Attempt register, should be same - layer2a, err := ls.Register(bytes.NewReader(tar2), layer1a.ChainID()) + layer2a, err := ls.Register(bytes.NewReader(tar2), layer1a.ChainID(), Platform(runtime.GOOS)) if err != nil { t.Fatal(err) } diff --git a/layer/ro_layer.go b/layer/ro_layer.go index 8b4cf8f0de..e03d78b4db 100644 --- a/layer/ro_layer.go +++ b/layer/ro_layer.go @@ -16,6 +16,7 @@ type roLayer struct { size int64 layerStore *layerStore descriptor distribution.Descriptor + platform Platform referenceCount int references map[Layer]struct{} @@ -142,6 +143,9 @@ func storeLayer(tx MetadataTransaction, layer *roLayer) error { return err } } + if err := tx.SetPlatform(layer.platform); err != nil { + return err + } return nil } diff --git a/layer/ro_layer_unix.go b/layer/ro_layer_unix.go new file mode 100644 index 0000000000..1b36856f9e --- /dev/null +++ b/layer/ro_layer_unix.go @@ -0,0 +1,7 @@ +// +build !windows + +package layer + +func (rl *roLayer) Platform() Platform { + return "" +} diff --git a/layer/ro_layer_windows.go b/layer/ro_layer_windows.go index 32bd7182a3..6679bdfe8f 100644 --- a/layer/ro_layer_windows.go +++ b/layer/ro_layer_windows.go @@ -7,3 +7,10 @@ var _ distribution.Describable = &roLayer{} func (rl *roLayer) Descriptor() distribution.Descriptor { return rl.descriptor } + +func (rl *roLayer) Platform() Platform { + if rl.platform == "" { + return "windows" + } + return rl.platform +} diff --git a/libcontainerd/client_windows.go b/libcontainerd/client_windows.go index 34f4c87d10..5349b66b5c 100644 --- a/libcontainerd/client_windows.go +++ b/libcontainerd/client_windows.go @@ -1,6 +1,7 @@ package libcontainerd import ( + "encoding/json" "errors" "fmt" "io" @@ -96,8 +97,17 @@ const defaultOwner = "docker" func (clnt *client) Create(containerID string, checkpoint string, checkpointDir string, spec specs.Spec, attachStdio StdioCallback, options ...CreateOption) error { clnt.lock(containerID) defer clnt.unlock(containerID) - logrus.Debugln("libcontainerd: client.Create() with spec", spec) + if b, err := json.Marshal(spec); err == nil { + logrus.Debugln("libcontainerd: client.Create() with spec", string(b)) + } + osName := spec.Platform.OS + if osName == "windows" { + return clnt.createWindows(containerID, checkpoint, checkpointDir, spec, attachStdio, options...) + } + return clnt.createLinux(containerID, checkpoint, checkpointDir, spec, attachStdio, options...) +} +func (clnt *client) createWindows(containerID string, checkpoint string, checkpointDir string, spec specs.Spec, attachStdio StdioCallback, options ...CreateOption) error { configuration := &hcsshim.ContainerConfig{ SystemType: "Container", Name: containerID, @@ -265,17 +275,100 @@ func (clnt *client) Create(containerID string, checkpoint string, checkpointDir // Call start, and if it fails, delete the container from our // internal structure, start will keep HCS in sync by deleting the // container there. - logrus.Debugf("libcontainerd: Create() id=%s, Calling start()", containerID) + logrus.Debugf("libcontainerd: createWindows() id=%s, Calling start()", containerID) if err := container.start(attachStdio); err != nil { clnt.deleteContainer(containerID) return err } - logrus.Debugf("libcontainerd: Create() id=%s completed successfully", containerID) + logrus.Debugf("libcontainerd: createWindows() id=%s completed successfully", containerID) return nil } +func (clnt *client) createLinux(containerID string, checkpoint string, checkpointDir string, spec specs.Spec, attachStdio StdioCallback, options ...CreateOption) error { + logrus.Debugf("libcontainerd: createLinux(): containerId %s ", containerID) + + // TODO @jhowardmsft LCOW Support: This needs to be configurable, not hard-coded. + // However, good-enough for the LCOW bring-up. + configuration := &hcsshim.ContainerConfig{ + HvPartition: true, + Name: containerID, + SystemType: "container", + ContainerType: "linux", + TerminateOnLastHandleClosed: true, + HvRuntime: &hcsshim.HvRuntime{ + ImagePath: `c:\program files\lcow`, + }, + } + + var layerOpt *LayerOption + for _, option := range options { + if l, ok := option.(*LayerOption); ok { + layerOpt = l + } + } + + // We must have a layer option with at least one path + if layerOpt == nil || layerOpt.LayerPaths == nil { + return fmt.Errorf("no layer option or paths were supplied to the runtime") + } + + // LayerFolderPath (writeable layer) + Layers (Guid + path) + configuration.LayerFolderPath = layerOpt.LayerFolderPath + for _, layerPath := range layerOpt.LayerPaths { + _, filename := filepath.Split(layerPath) + g, err := hcsshim.NameToGuid(filename) + if err != nil { + return err + } + configuration.Layers = append(configuration.Layers, hcsshim.Layer{ + ID: g.ToString(), + Path: filepath.Join(layerPath, "layer.vhd"), + }) + } + + hcsContainer, err := hcsshim.CreateContainer(containerID, configuration) + if err != nil { + return err + } + + // Construct a container object for calling start on it. + container := &container{ + containerCommon: containerCommon{ + process: process{ + processCommon: processCommon{ + containerID: containerID, + client: clnt, + friendlyName: InitFriendlyName, + }, + }, + processes: make(map[string]*process), + }, + ociSpec: spec, + hcsContainer: hcsContainer, + } + + container.options = options + for _, option := range options { + if err := option.Apply(container); err != nil { + logrus.Errorf("libcontainerd: createLinux() %v", err) + } + } + + // Call start, and if it fails, delete the container from our + // internal structure, start will keep HCS in sync by deleting the + // container there. + logrus.Debugf("libcontainerd: createLinux() id=%s, Calling start()", containerID) + if err := container.start(attachStdio); err != nil { + clnt.deleteContainer(containerID) + return err + } + + logrus.Debugf("libcontainerd: createLinux() id=%s completed successfully", containerID) + return nil +} + // AddProcess is the handler for adding a process to an already running // container. It's called through docker exec. It returns the system pid of the // exec'd process. @@ -292,13 +385,15 @@ func (clnt *client) AddProcess(ctx context.Context, containerID, processFriendly // create stdin, even if it's not used - it will be closed shortly. Stderr // is only created if it we're not -t. createProcessParms := hcsshim.ProcessConfig{ - EmulateConsole: procToAdd.Terminal, CreateStdInPipe: true, CreateStdOutPipe: true, CreateStdErrPipe: !procToAdd.Terminal, } - createProcessParms.ConsoleSize[0] = uint(procToAdd.ConsoleSize.Height) - createProcessParms.ConsoleSize[1] = uint(procToAdd.ConsoleSize.Width) + if procToAdd.Terminal { + createProcessParms.EmulateConsole = true + createProcessParms.ConsoleSize[0] = uint(procToAdd.ConsoleSize.Height) + createProcessParms.ConsoleSize[1] = uint(procToAdd.ConsoleSize.Width) + } // Take working directory from the process to add if it is defined, // otherwise take from the first process. diff --git a/libcontainerd/container_windows.go b/libcontainerd/container_windows.go index 753d2f2f92..3fd8b6793d 100644 --- a/libcontainerd/container_windows.go +++ b/libcontainerd/container_windows.go @@ -1,6 +1,7 @@ package libcontainerd import ( + "encoding/json" "fmt" "io" "io/ioutil" @@ -10,6 +11,7 @@ import ( "github.com/Microsoft/hcsshim" "github.com/Sirupsen/logrus" + "github.com/docker/docker/pkg/system" "github.com/opencontainers/runtime-spec/specs-go" ) @@ -83,6 +85,16 @@ func (ctr *container) start(attachStdio StdioCallback) error { createProcessParms.CommandLine = strings.Join(ctr.ociSpec.Process.Args, " ") createProcessParms.User = ctr.ociSpec.Process.User.Username + // LCOW requires the raw OCI spec passed through HCS and onwards to GCS for the utility VM. + if system.LCOWSupported() && ctr.ociSpec.Platform.OS == "linux" { + ociBuf, err := json.Marshal(ctr.ociSpec) + if err != nil { + return err + } + ociRaw := json.RawMessage(ociBuf) + createProcessParms.OCISpecification = &ociRaw + } + // Start the command running in the container. newProcess, err := ctr.hcsContainer.CreateProcess(createProcessParms) if err != nil { @@ -228,11 +240,14 @@ func (ctr *container) waitExit(process *process, isFirstProcessToStart bool) err if !isFirstProcessToStart { si.State = StateExitProcess } else { - updatePending, err := ctr.hcsContainer.HasPendingUpdates() - if err != nil { - logrus.Warnf("libcontainerd: HasPendingUpdates() failed (container may have been killed): %s", err) - } else { - si.UpdatePending = updatePending + // Pending updates is only applicable for WCOW + if ctr.ociSpec.Platform.OS == "windows" { + updatePending, err := ctr.hcsContainer.HasPendingUpdates() + if err != nil { + logrus.Warnf("libcontainerd: HasPendingUpdates() failed (container may have been killed): %s", err) + } else { + si.UpdatePending = updatePending + } } logrus.Debugf("libcontainerd: shutting down container %s", ctr.containerID) diff --git a/libcontainerd/remote_unix.go b/libcontainerd/remote_unix.go index e63fcc7a62..a81a93cbda 100644 --- a/libcontainerd/remote_unix.go +++ b/libcontainerd/remote_unix.go @@ -80,7 +80,7 @@ func New(stateDir string, options ...RemoteOption) (_ Remote, err error) { } } - if err := system.MkdirAll(stateDir, 0700); err != nil { + if err := system.MkdirAll(stateDir, 0700, ""); err != nil { return nil, err } diff --git a/migrate/v1/migratev1_test.go b/migrate/v1/migratev1_test.go index 55898f12b4..51b6741226 100644 --- a/migrate/v1/migratev1_test.go +++ b/migrate/v1/migratev1_test.go @@ -94,7 +94,7 @@ func TestMigrateContainers(t *testing.T) { t.Fatal(err) } - is, err := image.NewImageStore(ifs, ls) + is, err := image.NewImageStore(ifs, runtime.GOOS, ls) if err != nil { t.Fatal(err) } @@ -172,12 +172,12 @@ func TestMigrateImages(t *testing.T) { t.Fatal(err) } - is, err := image.NewImageStore(ifs, ls) + is, err := image.NewImageStore(ifs, runtime.GOOS, ls) if err != nil { t.Fatal(err) } - ms, err := metadata.NewFSMetadataStore(filepath.Join(tmpdir, "distribution")) + ms, err := metadata.NewFSMetadataStore(filepath.Join(tmpdir, "distribution"), runtime.GOOS) if err != nil { t.Fatal(err) } @@ -433,6 +433,10 @@ func (l *mockLayer) DiffSize() (int64, error) { return 0, nil } +func (l *mockLayer) Platform() layer.Platform { + return "" +} + func (l *mockLayer) Metadata() (map[string]string, error) { return nil, nil } diff --git a/oci/defaults_linux.go b/oci/defaults.go similarity index 75% rename from oci/defaults_linux.go rename to oci/defaults.go index c1ff931c22..4376faf5a7 100644 --- a/oci/defaults_linux.go +++ b/oci/defaults.go @@ -30,14 +30,55 @@ func defaultCapabilities() []string { } } -// DefaultSpec returns default oci spec used by docker. +// DefaultSpec returns the default spec used by docker for the current Platform func DefaultSpec() specs.Spec { - s := specs.Spec{ + return DefaultOSSpec(runtime.GOOS) +} + +// DefaultOSSpec returns the spec for a given OS +func DefaultOSSpec(osName string) specs.Spec { + if osName == "windows" { + return DefaultWindowsSpec() + } else if osName == "solaris" { + return DefaultSolarisSpec() + } else { + return DefaultLinuxSpec() + } +} + +// DefaultWindowsSpec create a default spec for running Windows containers +func DefaultWindowsSpec() specs.Spec { + return specs.Spec{ Version: specs.Version, Platform: specs.Platform{ OS: runtime.GOOS, Arch: runtime.GOARCH, }, + Windows: &specs.Windows{}, + } +} + +// DefaultSolarisSpec create a default spec for running Solaris containers +func DefaultSolarisSpec() specs.Spec { + s := specs.Spec{ + Version: "0.6.0", + Platform: specs.Platform{ + OS: "SunOS", + Arch: runtime.GOARCH, + }, + } + s.Solaris = &specs.Solaris{} + return s +} + +// DefaultLinuxSpec create a default spec for running Linux containers +func DefaultLinuxSpec() specs.Spec { + s := specs.Spec{ + Version: specs.Version, + Platform: specs.Platform{ + OS: "linux", + Arch: runtime.GOARCH, + }, } s.Mounts = []specs.Mount{ { @@ -91,7 +132,6 @@ func DefaultSpec() specs.Spec { "/proc/timer_list", "/proc/timer_stats", "/proc/sched_debug", - "/sys/firmware", }, ReadonlyPaths: []string{ "/proc/asound", @@ -172,5 +212,10 @@ func DefaultSpec() specs.Spec { }, } + // For LCOW support, don't mask /sys/firmware + if runtime.GOOS != "windows" { + s.Linux.MaskedPaths = append(s.Linux.MaskedPaths, "/sys/firmware") + } + return s } diff --git a/oci/defaults_solaris.go b/oci/defaults_solaris.go deleted file mode 100644 index 85c8b68e16..0000000000 --- a/oci/defaults_solaris.go +++ /dev/null @@ -1,20 +0,0 @@ -package oci - -import ( - "runtime" - - "github.com/opencontainers/runtime-spec/specs-go" -) - -// DefaultSpec returns default oci spec used by docker. -func DefaultSpec() specs.Spec { - s := specs.Spec{ - Version: "0.6.0", - Platform: specs.Platform{ - OS: "SunOS", - Arch: runtime.GOARCH, - }, - } - s.Solaris = &specs.Solaris{} - return s -} diff --git a/oci/defaults_windows.go b/oci/defaults_windows.go deleted file mode 100644 index ab51904ec4..0000000000 --- a/oci/defaults_windows.go +++ /dev/null @@ -1,19 +0,0 @@ -package oci - -import ( - "runtime" - - "github.com/opencontainers/runtime-spec/specs-go" -) - -// DefaultSpec returns default spec used by docker. -func DefaultSpec() specs.Spec { - return specs.Spec{ - Version: specs.Version, - Platform: specs.Platform{ - OS: runtime.GOOS, - Arch: runtime.GOARCH, - }, - Windows: &specs.Windows{}, - } -} diff --git a/pkg/archive/archive.go b/pkg/archive/archive.go index 5fb774d602..06e8e7e8bb 100644 --- a/pkg/archive/archive.go +++ b/pkg/archive/archive.go @@ -1035,7 +1035,7 @@ func (archiver *Archiver) CopyFileWithTar(src, dst string) (err error) { dst = filepath.Join(dst, filepath.Base(src)) } // Create the holding directory if necessary - if err := system.MkdirAll(filepath.Dir(dst), 0700); err != nil { + if err := system.MkdirAll(filepath.Dir(dst), 0700, ""); err != nil { return err } diff --git a/pkg/archive/diff.go b/pkg/archive/diff.go index d20854e2a2..a2766b5928 100644 --- a/pkg/archive/diff.go +++ b/pkg/archive/diff.go @@ -84,7 +84,7 @@ func UnpackLayer(dest string, layer io.Reader, options *TarOptions) (size int64, parentPath := filepath.Join(dest, parent) if _, err := os.Lstat(parentPath); err != nil && os.IsNotExist(err) { - err = system.MkdirAll(parentPath, 0600) + err = system.MkdirAll(parentPath, 0600, "") if err != nil { return 0, err } diff --git a/pkg/chrootarchive/archive_test.go b/pkg/chrootarchive/archive_test.go index 4780f3be08..bd2deb2dd9 100644 --- a/pkg/chrootarchive/archive_test.go +++ b/pkg/chrootarchive/archive_test.go @@ -47,7 +47,7 @@ func TestChrootTarUntar(t *testing.T) { } defer os.RemoveAll(tmpdir) src := filepath.Join(tmpdir, "src") - if err := system.MkdirAll(src, 0700); err != nil { + if err := system.MkdirAll(src, 0700, ""); err != nil { t.Fatal(err) } if err := ioutil.WriteFile(filepath.Join(src, "toto"), []byte("hello toto"), 0644); err != nil { @@ -61,7 +61,7 @@ func TestChrootTarUntar(t *testing.T) { t.Fatal(err) } dest := filepath.Join(tmpdir, "src") - if err := system.MkdirAll(dest, 0700); err != nil { + if err := system.MkdirAll(dest, 0700, ""); err != nil { t.Fatal(err) } if err := Untar(stream, dest, &archive.TarOptions{ExcludePatterns: []string{"lolo"}}); err != nil { @@ -78,7 +78,7 @@ func TestChrootUntarWithHugeExcludesList(t *testing.T) { } defer os.RemoveAll(tmpdir) src := filepath.Join(tmpdir, "src") - if err := system.MkdirAll(src, 0700); err != nil { + if err := system.MkdirAll(src, 0700, ""); err != nil { t.Fatal(err) } if err := ioutil.WriteFile(filepath.Join(src, "toto"), []byte("hello toto"), 0644); err != nil { @@ -89,7 +89,7 @@ func TestChrootUntarWithHugeExcludesList(t *testing.T) { t.Fatal(err) } dest := filepath.Join(tmpdir, "dest") - if err := system.MkdirAll(dest, 0700); err != nil { + if err := system.MkdirAll(dest, 0700, ""); err != nil { t.Fatal(err) } options := &archive.TarOptions{} @@ -180,7 +180,7 @@ func TestChrootTarUntarWithSymlink(t *testing.T) { } defer os.RemoveAll(tmpdir) src := filepath.Join(tmpdir, "src") - if err := system.MkdirAll(src, 0700); err != nil { + if err := system.MkdirAll(src, 0700, ""); err != nil { t.Fatal(err) } if _, err := prepareSourceDirectory(10, src, false); err != nil { @@ -206,7 +206,7 @@ func TestChrootCopyWithTar(t *testing.T) { } defer os.RemoveAll(tmpdir) src := filepath.Join(tmpdir, "src") - if err := system.MkdirAll(src, 0700); err != nil { + if err := system.MkdirAll(src, 0700, ""); err != nil { t.Fatal(err) } if _, err := prepareSourceDirectory(10, src, true); err != nil { @@ -252,7 +252,7 @@ func TestChrootCopyFileWithTar(t *testing.T) { } defer os.RemoveAll(tmpdir) src := filepath.Join(tmpdir, "src") - if err := system.MkdirAll(src, 0700); err != nil { + if err := system.MkdirAll(src, 0700, ""); err != nil { t.Fatal(err) } if _, err := prepareSourceDirectory(10, src, true); err != nil { @@ -299,7 +299,7 @@ func TestChrootUntarPath(t *testing.T) { } defer os.RemoveAll(tmpdir) src := filepath.Join(tmpdir, "src") - if err := system.MkdirAll(src, 0700); err != nil { + if err := system.MkdirAll(src, 0700, ""); err != nil { t.Fatal(err) } if _, err := prepareSourceDirectory(10, src, false); err != nil { @@ -360,7 +360,7 @@ func TestChrootUntarEmptyArchiveFromSlowReader(t *testing.T) { } defer os.RemoveAll(tmpdir) dest := filepath.Join(tmpdir, "dest") - if err := system.MkdirAll(dest, 0700); err != nil { + if err := system.MkdirAll(dest, 0700, ""); err != nil { t.Fatal(err) } stream := &slowEmptyTarReader{size: 10240, chunkSize: 1024} @@ -376,7 +376,7 @@ func TestChrootApplyEmptyArchiveFromSlowReader(t *testing.T) { } defer os.RemoveAll(tmpdir) dest := filepath.Join(tmpdir, "dest") - if err := system.MkdirAll(dest, 0700); err != nil { + if err := system.MkdirAll(dest, 0700, ""); err != nil { t.Fatal(err) } stream := &slowEmptyTarReader{size: 10240, chunkSize: 1024} @@ -392,7 +392,7 @@ func TestChrootApplyDotDotFile(t *testing.T) { } defer os.RemoveAll(tmpdir) src := filepath.Join(tmpdir, "src") - if err := system.MkdirAll(src, 0700); err != nil { + if err := system.MkdirAll(src, 0700, ""); err != nil { t.Fatal(err) } if err := ioutil.WriteFile(filepath.Join(src, "..gitme"), []byte(""), 0644); err != nil { @@ -403,7 +403,7 @@ func TestChrootApplyDotDotFile(t *testing.T) { t.Fatal(err) } dest := filepath.Join(tmpdir, "dest") - if err := system.MkdirAll(dest, 0700); err != nil { + if err := system.MkdirAll(dest, 0700, ""); err != nil { t.Fatal(err) } if _, err := ApplyLayer(dest, stream); err != nil { diff --git a/pkg/idtools/idtools_unix.go b/pkg/idtools/idtools_unix.go index 0b28249fa8..8701bb7fa9 100644 --- a/pkg/idtools/idtools_unix.go +++ b/pkg/idtools/idtools_unix.go @@ -49,7 +49,7 @@ func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chown paths = append(paths, dirPath) } } - if err := system.MkdirAll(path, mode); err != nil && !os.IsExist(err) { + if err := system.MkdirAll(path, mode, ""); err != nil && !os.IsExist(err) { return err } } else { diff --git a/pkg/idtools/idtools_windows.go b/pkg/idtools/idtools_windows.go index 8ed8353060..45d2878e38 100644 --- a/pkg/idtools/idtools_windows.go +++ b/pkg/idtools/idtools_windows.go @@ -11,7 +11,7 @@ import ( // Platforms such as Windows do not support the UID/GID concept. So make this // just a wrapper around system.MkdirAll. func mkdirAs(path string, mode os.FileMode, ownerUID, ownerGID int, mkAll, chownExisting bool) error { - if err := system.MkdirAll(path, mode); err != nil && !os.IsExist(err) { + if err := system.MkdirAll(path, mode, ""); err != nil && !os.IsExist(err) { return err } return nil diff --git a/pkg/pidfile/pidfile.go b/pkg/pidfile/pidfile.go index 88a195835c..0fc3997a11 100644 --- a/pkg/pidfile/pidfile.go +++ b/pkg/pidfile/pidfile.go @@ -37,7 +37,7 @@ func New(path string) (*PIDFile, error) { return nil, err } // Note MkdirAll returns nil if a directory already exists - if err := system.MkdirAll(filepath.Dir(path), os.FileMode(0755)); err != nil { + if err := system.MkdirAll(filepath.Dir(path), os.FileMode(0755), ""); err != nil { return nil, err } if err := ioutil.WriteFile(path, []byte(fmt.Sprintf("%d", os.Getpid())), 0644); err != nil { diff --git a/pkg/system/chtimes.go b/pkg/system/chtimes.go index 7637f12e1a..056d19954d 100644 --- a/pkg/system/chtimes.go +++ b/pkg/system/chtimes.go @@ -2,26 +2,9 @@ package system import ( "os" - "syscall" "time" - "unsafe" ) -var ( - maxTime time.Time -) - -func init() { - if unsafe.Sizeof(syscall.Timespec{}.Nsec) == 8 { - // This is a 64 bit timespec - // os.Chtimes limits time to the following - maxTime = time.Unix(0, 1<<63-1) - } else { - // This is a 32 bit timespec - maxTime = time.Unix(1<<31-1, 0) - } -} - // Chtimes changes the access time and modified time of a file at the given path func Chtimes(name string, atime time.Time, mtime time.Time) error { unixMinTime := time.Unix(0, 0) diff --git a/pkg/system/filesys.go b/pkg/system/filesys.go index 7aa920de1a..102565f760 100644 --- a/pkg/system/filesys.go +++ b/pkg/system/filesys.go @@ -8,15 +8,14 @@ import ( "path/filepath" ) -// MkdirAllWithACL is a wrapper for MkdirAll that creates a directory -// ACL'd for Builtin Administrators and Local System. -func MkdirAllWithACL(path string, perm os.FileMode) error { - return MkdirAll(path, perm) +// MkdirAllWithACL is a wrapper for MkdirAll on unix systems. +func MkdirAllWithACL(path string, perm os.FileMode, sddl string) error { + return MkdirAll(path, perm, sddl) } // MkdirAll creates a directory named path along with any necessary parents, // with permission specified by attribute perm for all dir created. -func MkdirAll(path string, perm os.FileMode) error { +func MkdirAll(path string, perm os.FileMode, sddl string) error { return os.MkdirAll(path, perm) } diff --git a/pkg/system/filesys_windows.go b/pkg/system/filesys_windows.go index 626d2ad886..20117db919 100644 --- a/pkg/system/filesys_windows.go +++ b/pkg/system/filesys_windows.go @@ -16,21 +16,28 @@ import ( winio "github.com/Microsoft/go-winio" ) +const ( + // SddlAdministratorsLocalSystem is local administrators plus NT AUTHORITY\System + SddlAdministratorsLocalSystem = "D:P(A;OICI;GA;;;BA)(A;OICI;GA;;;SY)" + // SddlNtvmAdministratorsLocalSystem is NT VIRTUAL MACHINE\Virtual Machines plus local administrators plus NT AUTHORITY\System + SddlNtvmAdministratorsLocalSystem = "D:P(A;OICI;GA;;;S-1-5-83-0)(A;OICI;GA;;;BA)(A;OICI;GA;;;SY)" +) + // MkdirAllWithACL is a wrapper for MkdirAll that creates a directory -// ACL'd for Builtin Administrators and Local System. -func MkdirAllWithACL(path string, perm os.FileMode) error { - return mkdirall(path, true) +// with an appropriate SDDL defined ACL. +func MkdirAllWithACL(path string, perm os.FileMode, sddl string) error { + return mkdirall(path, true, sddl) } // MkdirAll implementation that is volume path aware for Windows. -func MkdirAll(path string, _ os.FileMode) error { - return mkdirall(path, false) +func MkdirAll(path string, _ os.FileMode, sddl string) error { + return mkdirall(path, false, sddl) } // mkdirall is a custom version of os.MkdirAll modified for use on Windows // so that it is both volume path aware, and can create a directory with // a DACL. -func mkdirall(path string, adminAndLocalSystem bool) error { +func mkdirall(path string, applyACL bool, sddl string) error { if re := regexp.MustCompile(`^\\\\\?\\Volume{[a-z0-9-]+}$`); re.MatchString(path) { return nil } @@ -64,15 +71,15 @@ func mkdirall(path string, adminAndLocalSystem bool) error { if j > 1 { // Create parent - err = mkdirall(path[0:j-1], false) + err = mkdirall(path[0:j-1], false, sddl) if err != nil { return err } } // Parent now exists; invoke os.Mkdir or mkdirWithACL and use its result. - if adminAndLocalSystem { - err = mkdirWithACL(path) + if applyACL { + err = mkdirWithACL(path, sddl) } else { err = os.Mkdir(path, 0) } @@ -96,9 +103,9 @@ func mkdirall(path string, adminAndLocalSystem bool) error { // in golang to cater for creating a directory am ACL permitting full // access, with inheritance, to any subfolder/file for Built-in Administrators // and Local System. -func mkdirWithACL(name string) error { +func mkdirWithACL(name string, sddl string) error { sa := syscall.SecurityAttributes{Length: 0} - sddl := "D:P(A;OICI;GA;;;BA)(A;OICI;GA;;;SY)" + sd, err := winio.SddlToSecurityDescriptor(sddl) if err != nil { return &os.PathError{Op: "mkdir", Path: name, Err: err} diff --git a/pkg/system/init.go b/pkg/system/init.go new file mode 100644 index 0000000000..17935088de --- /dev/null +++ b/pkg/system/init.go @@ -0,0 +1,22 @@ +package system + +import ( + "syscall" + "time" + "unsafe" +) + +// Used by chtimes +var maxTime time.Time + +func init() { + // chtimes initialization + if unsafe.Sizeof(syscall.Timespec{}.Nsec) == 8 { + // This is a 64 bit timespec + // os.Chtimes limits time to the following + maxTime = time.Unix(0, 1<<63-1) + } else { + // This is a 32 bit timespec + maxTime = time.Unix(1<<31-1, 0) + } +} diff --git a/pkg/system/init_windows.go b/pkg/system/init_windows.go new file mode 100644 index 0000000000..019c66441c --- /dev/null +++ b/pkg/system/init_windows.go @@ -0,0 +1,17 @@ +package system + +import "os" + +// LCOWSupported determines if Linux Containers on Windows are supported. +// Note: This feature is in development (06/17) and enabled through an +// environment variable. At a future time, it will be enabled based +// on build number. @jhowardmsft +var lcowSupported = false + +func init() { + // LCOW initialization + if os.Getenv("LCOW_SUPPORTED") != "" { + lcowSupported = true + } + +} diff --git a/pkg/system/lcow_unix.go b/pkg/system/lcow_unix.go new file mode 100644 index 0000000000..cff33bb408 --- /dev/null +++ b/pkg/system/lcow_unix.go @@ -0,0 +1,8 @@ +// +build !windows + +package system + +// LCOWSupported returns true if Linux containers on Windows are supported. +func LCOWSupported() bool { + return false +} diff --git a/pkg/system/lcow_windows.go b/pkg/system/lcow_windows.go new file mode 100644 index 0000000000..e54d01e696 --- /dev/null +++ b/pkg/system/lcow_windows.go @@ -0,0 +1,6 @@ +package system + +// LCOWSupported returns true if Linux containers on Windows are supported. +func LCOWSupported() bool { + return lcowSupported +} diff --git a/pkg/system/path.go b/pkg/system/path.go new file mode 100644 index 0000000000..f634a6be67 --- /dev/null +++ b/pkg/system/path.go @@ -0,0 +1,21 @@ +package system + +import "runtime" + +const defaultUnixPathEnv = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + +// DefaultPathEnv is unix style list of directories to search for +// executables. Each directory is separated from the next by a colon +// ':' character . +func DefaultPathEnv(platform string) string { + if runtime.GOOS == "windows" { + if platform != runtime.GOOS && LCOWSupported() { + return defaultUnixPathEnv + } + // Deliberately empty on Windows containers on Windows as the default path will be set by + // the container. Docker has no context of what the default path should be. + return "" + } + return defaultUnixPathEnv + +} diff --git a/pkg/system/path_unix.go b/pkg/system/path_unix.go index c607c4db09..f3762e69d3 100644 --- a/pkg/system/path_unix.go +++ b/pkg/system/path_unix.go @@ -2,11 +2,6 @@ package system -// DefaultPathEnv is unix style list of directories to search for -// executables. Each directory is separated from the next by a colon -// ':' character . -const DefaultPathEnv = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" - // CheckSystemDriveAndRemoveDriveLetter verifies that a path, if it includes a drive letter, // is the system drive. This is a no-op on Linux. func CheckSystemDriveAndRemoveDriveLetter(path string) (string, error) { diff --git a/pkg/system/path_windows.go b/pkg/system/path_windows.go index cbfe2c1576..3fc4744948 100644 --- a/pkg/system/path_windows.go +++ b/pkg/system/path_windows.go @@ -8,10 +8,6 @@ import ( "strings" ) -// DefaultPathEnv is deliberately empty on Windows as the default path will be set by -// the container. Docker has no context of what the default path should be. -const DefaultPathEnv = "" - // CheckSystemDriveAndRemoveDriveLetter verifies and manipulates a Windows path. // This is used, for example, when validating a user provided path in docker cp. // If a drive letter is supplied, it must be the system drive. The drive letter diff --git a/plugin/backend_linux.go b/plugin/backend_linux.go index 012f6cf22b..c2baab5c94 100644 --- a/plugin/backend_linux.go +++ b/plugin/backend_linux.go @@ -145,7 +145,7 @@ func (s *tempConfigStore) Get(d digest.Digest) ([]byte, error) { return s.config, nil } -func (s *tempConfigStore) RootFSFromConfig(c []byte) (*image.RootFS, error) { +func (s *tempConfigStore) RootFSAndPlatformFromConfig(c []byte) (*image.RootFS, layer.Platform, error) { return configToRootFS(c) } @@ -525,7 +525,7 @@ func (s *pluginConfigStore) Get(d digest.Digest) ([]byte, error) { return ioutil.ReadAll(rwc) } -func (s *pluginConfigStore) RootFSFromConfig(c []byte) (*image.RootFS, error) { +func (s *pluginConfigStore) RootFSAndPlatformFromConfig(c []byte) (*image.RootFS, layer.Platform, error) { return configToRootFS(c) } diff --git a/plugin/blobstore.go b/plugin/blobstore.go index 172f6b235e..2b79a44270 100644 --- a/plugin/blobstore.go +++ b/plugin/blobstore.go @@ -126,7 +126,8 @@ type downloadManager struct { configDigest digest.Digest } -func (dm *downloadManager) Download(ctx context.Context, initialRootFS image.RootFS, layers []xfer.DownloadDescriptor, progressOutput progress.Output) (image.RootFS, func(), error) { +func (dm *downloadManager) Download(ctx context.Context, initialRootFS image.RootFS, platform layer.Platform, layers []xfer.DownloadDescriptor, progressOutput progress.Output) (image.RootFS, func(), error) { + // TODO @jhowardmsft LCOW: May need revisiting. for _, l := range layers { b, err := dm.blobStore.New() if err != nil { @@ -178,6 +179,6 @@ func (dm *downloadManager) Put(dt []byte) (digest.Digest, error) { func (dm *downloadManager) Get(d digest.Digest) ([]byte, error) { return nil, fmt.Errorf("digest not found") } -func (dm *downloadManager) RootFSFromConfig(c []byte) (*image.RootFS, error) { +func (dm *downloadManager) RootFSAndPlatformFromConfig(c []byte) (*image.RootFS, layer.Platform, error) { return configToRootFS(c) } diff --git a/plugin/manager.go b/plugin/manager.go index f1c5788a9f..aa090f5a90 100644 --- a/plugin/manager.go +++ b/plugin/manager.go @@ -8,6 +8,7 @@ import ( "path/filepath" "reflect" "regexp" + "runtime" "sort" "strings" "sync" @@ -21,6 +22,7 @@ import ( "github.com/docker/docker/pkg/authorization" "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/mount" + "github.com/docker/docker/pkg/system" "github.com/docker/docker/plugin/v2" "github.com/docker/docker/registry" "github.com/opencontainers/go-digest" @@ -348,17 +350,22 @@ func isEqualPrivilege(a, b types.PluginPrivilege) bool { return reflect.DeepEqual(a.Value, b.Value) } -func configToRootFS(c []byte) (*image.RootFS, error) { +func configToRootFS(c []byte) (*image.RootFS, layer.Platform, error) { + // TODO @jhowardmsft LCOW - Will need to revisit this. For now, calculate the platform. + platform := layer.Platform(runtime.GOOS) + if platform == "windows" && system.LCOWSupported() { + platform = "linux" + } var pluginConfig types.PluginConfig if err := json.Unmarshal(c, &pluginConfig); err != nil { - return nil, err + return nil, "", err } // validation for empty rootfs is in distribution code if pluginConfig.Rootfs == nil { - return nil, nil + return nil, platform, nil } - return rootFSFromPlugin(pluginConfig.Rootfs), nil + return rootFSFromPlugin(pluginConfig.Rootfs), platform, nil } func rootFSFromPlugin(pluginfs *types.PluginConfigRootfs) *image.RootFS { diff --git a/plugin/v2/plugin_linux.go b/plugin/v2/plugin_linux.go index 412818ed33..9cae180e33 100644 --- a/plugin/v2/plugin_linux.go +++ b/plugin/v2/plugin_linux.go @@ -5,6 +5,7 @@ package v2 import ( "os" "path/filepath" + "runtime" "strings" "github.com/docker/docker/api/types" @@ -108,7 +109,7 @@ func (p *Plugin) InitSpec(execRoot string) (*specs.Spec, error) { } envs := make([]string, 1, len(p.PluginObj.Settings.Env)+1) - envs[0] = "PATH=" + system.DefaultPathEnv + envs[0] = "PATH=" + system.DefaultPathEnv(runtime.GOOS) envs = append(envs, p.PluginObj.Settings.Env...) args := append(p.PluginObj.Config.Entrypoint, p.PluginObj.Settings.Args...) diff --git a/reference/store.go b/reference/store.go index 8466e6e2cc..6599235401 100644 --- a/reference/store.go +++ b/reference/store.go @@ -46,6 +46,9 @@ type store struct { // referencesByIDCache is a cache of references indexed by ID, to speed // up References. referencesByIDCache map[digest.Digest]map[string]reference.Named + // platform is the container target platform for this store (which may be + // different to the host operating system + platform string } // Repository maps tags to digests. The key is a stringified Reference, @@ -70,7 +73,7 @@ func (a lexicalAssociations) Less(i, j int) bool { // NewReferenceStore creates a new reference store, tied to a file path where // the set of references are serialized in JSON format. -func NewReferenceStore(jsonPath string) (Store, error) { +func NewReferenceStore(jsonPath, platform string) (Store, error) { abspath, err := filepath.Abs(jsonPath) if err != nil { return nil, err @@ -80,6 +83,7 @@ func NewReferenceStore(jsonPath string) (Store, error) { jsonPath: abspath, Repositories: make(map[string]repository), referencesByIDCache: make(map[digest.Digest]map[string]reference.Named), + platform: platform, } // Load the json file if it exists, otherwise create it. if err := store.reload(); os.IsNotExist(err) { diff --git a/reference/store_test.go b/reference/store_test.go index 8f0ff6304e..2c796e76f9 100644 --- a/reference/store_test.go +++ b/reference/store_test.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" "path/filepath" + "runtime" "strings" "testing" @@ -40,7 +41,7 @@ func TestLoad(t *testing.T) { } jsonFile.Close() - store, err := NewReferenceStore(jsonFile.Name()) + store, err := NewReferenceStore(jsonFile.Name(), runtime.GOOS) if err != nil { t.Fatalf("error creating tag store: %v", err) } @@ -69,7 +70,7 @@ func TestSave(t *testing.T) { jsonFile.Close() defer os.RemoveAll(jsonFile.Name()) - store, err := NewReferenceStore(jsonFile.Name()) + store, err := NewReferenceStore(jsonFile.Name(), runtime.GOOS) if err != nil { t.Fatalf("error creating tag store: %v", err) } @@ -111,7 +112,7 @@ func TestAddDeleteGet(t *testing.T) { jsonFile.Close() defer os.RemoveAll(jsonFile.Name()) - store, err := NewReferenceStore(jsonFile.Name()) + store, err := NewReferenceStore(jsonFile.Name(), runtime.GOOS) if err != nil { t.Fatalf("error creating tag store: %v", err) } @@ -328,7 +329,7 @@ func TestInvalidTags(t *testing.T) { tmpDir, err := ioutil.TempDir("", "tag-store-test") defer os.RemoveAll(tmpDir) - store, err := NewReferenceStore(filepath.Join(tmpDir, "repositories.json")) + store, err := NewReferenceStore(filepath.Join(tmpDir, "repositories.json"), runtime.GOOS) if err != nil { t.Fatalf("error creating tag store: %v", err) } diff --git a/vendor.conf b/vendor.conf index e524c518ab..9bd91820bd 100644 --- a/vendor.conf +++ b/vendor.conf @@ -1,6 +1,6 @@ # the following lines are in sorted order, FYI github.com/Azure/go-ansiterm 388960b655244e76e24c75f48631564eaefade62 -github.com/Microsoft/hcsshim v0.5.17 +github.com/Microsoft/hcsshim v0.5.23 github.com/Microsoft/go-winio v0.4.2 github.com/Sirupsen/logrus v0.11.0 github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76 @@ -8,6 +8,7 @@ github.com/docker/libtrust 9cbd2a1374f46905c68a4eb3694a130610adc62a github.com/go-check/check 4ed411733c5785b40214c70bce814c3a3a689609 https://github.com/cpuguy83/check.git github.com/gorilla/context v1.1 github.com/gorilla/mux v1.1 +github.com/jhowardmsft/opengcs v0.0.3 github.com/kr/pty 5cf931ef8f github.com/mattn/go-shellwords v1.0.3 github.com/tchap/go-patricia v2.2.6 diff --git a/vendor/github.com/Microsoft/hcsshim/container.go b/vendor/github.com/Microsoft/hcsshim/container.go index 3a19519fd6..8bc7d6d2b7 100644 --- a/vendor/github.com/Microsoft/hcsshim/container.go +++ b/vendor/github.com/Microsoft/hcsshim/container.go @@ -35,7 +35,6 @@ type ContainerProperties struct { SystemType string Owner string SiloGUID string `json:"SiloGuid,omitempty"` - IsDummy bool `json:",omitempty"` RuntimeID string `json:"RuntimeId,omitempty"` IsRuntimeTemplate bool `json:",omitempty"` RuntimeImagePath string `json:",omitempty"` @@ -121,7 +120,7 @@ const ( // Supported resource types are Network and Request Types are Add/Remove type ResourceModificationRequestResponse struct { Resource ResourceType `json:"ResourceType"` - Data string `json:"Settings"` + Data interface{} `json:"Settings"` Request RequestType `json:"RequestType,omitempty"` } @@ -585,7 +584,7 @@ func (container *container) CreateProcess(c *ProcessConfig) (Process, error) { return nil, makeContainerError(container, operation, "", err) } - logrus.Debugf(title+" succeeded id=%s processid=%s", container.id, process.processID) + logrus.Debugf(title+" succeeded id=%s processid=%d", container.id, process.processID) return process, nil } diff --git a/vendor/github.com/Microsoft/hcsshim/interface.go b/vendor/github.com/Microsoft/hcsshim/interface.go index d981c4fc4c..a499fe46a0 100644 --- a/vendor/github.com/Microsoft/hcsshim/interface.go +++ b/vendor/github.com/Microsoft/hcsshim/interface.go @@ -1,6 +1,7 @@ package hcsshim import ( + "encoding/json" "io" "time" ) @@ -8,16 +9,18 @@ import ( // ProcessConfig is used as both the input of Container.CreateProcess // and to convert the parameters to JSON for passing onto the HCS type ProcessConfig struct { - ApplicationName string - CommandLine string - User string - WorkingDirectory string - Environment map[string]string - EmulateConsole bool - CreateStdInPipe bool - CreateStdOutPipe bool - CreateStdErrPipe bool - ConsoleSize [2]uint + ApplicationName string `json:",omitempty"` + CommandLine string `json:",omitempty"` + User string `json:",omitempty"` + WorkingDirectory string `json:",omitempty"` + Environment map[string]string `json:",omitempty"` + EmulateConsole bool `json:",omitempty"` + CreateStdInPipe bool `json:",omitempty"` + CreateStdOutPipe bool `json:",omitempty"` + CreateStdErrPipe bool `json:",omitempty"` + ConsoleSize [2]uint `json:",omitempty"` + CreateInUtilityVm bool `json:",omitempty"` // Used by Linux Containers on Windows + OCISpecification *json.RawMessage `json:",omitempty"` // Used by Linux Containers on Windows } type Layer struct { @@ -34,39 +37,50 @@ type MappedDir struct { } type HvRuntime struct { - ImagePath string `json:",omitempty"` - SkipTemplate bool `json:",omitempty"` + ImagePath string `json:",omitempty"` + SkipTemplate bool `json:",omitempty"` + LinuxInitrdPath string `json:",omitempty"` // Host path to an initrd image for starting a Linux utility VM + LinuxKernelPath string `json:",omitempty"` // Host path to kernel for starting a Linux utility VM +} + +type MappedVirtualDisk struct { + HostPath string `json:",omitempty"` // Path to VHD on the host + ContainerPath string // Platform-specific mount point path in the container + CreateInUtilityVM bool `json:",omitempty"` + ReadOnly bool `json:",omitempty"` + Cache string `json:",omitempty"` // "" (Unspecified); "Disabled"; "Enabled"; "Private"; "PrivateAllowSharing" } // ContainerConfig is used as both the input of CreateContainer // and to convert the parameters to JSON for passing onto the HCS type ContainerConfig struct { - SystemType string // HCS requires this to be hard-coded to "Container" - Name string // Name of the container. We use the docker ID. - Owner string // The management platform that created this container - IsDummy bool // Used for development purposes. - VolumePath string `json:",omitempty"` // Windows volume path for scratch space. Used by Windows Server Containers only. Format \\?\\Volume{GUID} - IgnoreFlushesDuringBoot bool // Optimization hint for container startup in Windows - LayerFolderPath string `json:",omitempty"` // Where the layer folders are located. Used by Windows Server Containers only. Format %root%\windowsfilter\containerID - Layers []Layer // List of storage layers. Required for Windows Server and Hyper-V Containers. Format ID=GUID;Path=%root%\windowsfilter\layerID - Credentials string `json:",omitempty"` // Credentials information - ProcessorCount uint32 `json:",omitempty"` // Number of processors to assign to the container. - ProcessorWeight uint64 `json:",omitempty"` // CPU Shares 0..10000 on Windows; where 0 will be omitted and HCS will default. - ProcessorMaximum int64 `json:",omitempty"` // CPU maximum usage percent 1..100 - StorageIOPSMaximum uint64 `json:",omitempty"` // Maximum Storage IOPS - StorageBandwidthMaximum uint64 `json:",omitempty"` // Maximum Storage Bandwidth in bytes per second - StorageSandboxSize uint64 `json:",omitempty"` // Size in bytes that the container system drive should be expanded to if smaller - MemoryMaximumInMB int64 `json:",omitempty"` // Maximum memory available to the container in Megabytes - HostName string // Hostname - MappedDirectories []MappedDir // List of mapped directories (volumes/mounts) - SandboxPath string `json:",omitempty"` // Location of unmounted sandbox. Used by Hyper-V containers only. Format %root%\windowsfilter - HvPartition bool // True if it a Hyper-V Container - EndpointList []string // List of networking endpoints to be attached to container - NetworkSharedContainerName string `json:",omitempty"` // Name (ID) of the container that we will share the network stack with. - HvRuntime *HvRuntime `json:",omitempty"` // Hyper-V container settings. Used by Hyper-V containers only. Format ImagePath=%root%\BaseLayerID\UtilityVM - Servicing bool // True if this container is for servicing - AllowUnqualifiedDNSQuery bool // True to allow unqualified DNS name resolution - DNSSearchList string `json:",omitempty"` // Comma seperated list of DNS suffixes to use for name resolution + SystemType string // HCS requires this to be hard-coded to "Container" + Name string // Name of the container. We use the docker ID. + Owner string `json:",omitempty"` // The management platform that created this container + VolumePath string `json:",omitempty"` // Windows volume path for scratch space. Used by Windows Server Containers only. Format \\?\\Volume{GUID} + IgnoreFlushesDuringBoot bool `json:",omitempty"` // Optimization hint for container startup in Windows + LayerFolderPath string `json:",omitempty"` // Where the layer folders are located. Used by Windows Server Containers only. Format %root%\windowsfilter\containerID + Layers []Layer // List of storage layers. Required for Windows Server and Hyper-V Containers. Format ID=GUID;Path=%root%\windowsfilter\layerID + Credentials string `json:",omitempty"` // Credentials information + ProcessorCount uint32 `json:",omitempty"` // Number of processors to assign to the container. + ProcessorWeight uint64 `json:",omitempty"` // CPU Shares 0..10000 on Windows; where 0 will be omitted and HCS will default. + ProcessorMaximum int64 `json:",omitempty"` // CPU maximum usage percent 1..100 + StorageIOPSMaximum uint64 `json:",omitempty"` // Maximum Storage IOPS + StorageBandwidthMaximum uint64 `json:",omitempty"` // Maximum Storage Bandwidth in bytes per second + StorageSandboxSize uint64 `json:",omitempty"` // Size in bytes that the container system drive should be expanded to if smaller + MemoryMaximumInMB int64 `json:",omitempty"` // Maximum memory available to the container in Megabytes + HostName string `json:",omitempty"` // Hostname + MappedDirectories []MappedDir `json:",omitempty"` // List of mapped directories (volumes/mounts) + HvPartition bool // True if it a Hyper-V Container + NetworkSharedContainerName string `json:",omitempty"` // Name (ID) of the container that we will share the network stack with. + EndpointList []string `json:",omitempty"` // List of networking endpoints to be attached to container + HvRuntime *HvRuntime `json:",omitempty"` // Hyper-V container settings. Used by Hyper-V containers only. Format ImagePath=%root%\BaseLayerID\UtilityVM + Servicing bool `json:",omitempty"` // True if this container is for servicing + AllowUnqualifiedDNSQuery bool `json:",omitempty"` // True to allow unqualified DNS name resolution + DNSSearchList string `json:",omitempty"` // Comma seperated list of DNS suffixes to use for name resolution + ContainerType string `json:",omitempty"` // "Linux" for Linux containers on Windows. Omitted otherwise. + TerminateOnLastHandleClosed bool `json:",omitempty"` // Should HCS terminate the container once all handles have been closed + MappedVirtualDisks []MappedVirtualDisk `json:",omitempty"` // Array of virtual disks to mount at start } type ComputeSystemQuery struct { diff --git a/vendor/github.com/Microsoft/hcsshim/waithelper.go b/vendor/github.com/Microsoft/hcsshim/waithelper.go index 89c94616ad..828d148e58 100644 --- a/vendor/github.com/Microsoft/hcsshim/waithelper.go +++ b/vendor/github.com/Microsoft/hcsshim/waithelper.go @@ -59,4 +59,5 @@ func waitForNotification(callbackNumber uintptr, expectedNotification hcsNotific case <-c: return ErrTimeout } + return nil } diff --git a/vendor/github.com/jhowardmsft/opengcs/LICENSE b/vendor/github.com/jhowardmsft/opengcs/LICENSE new file mode 100644 index 0000000000..8739a025ea --- /dev/null +++ b/vendor/github.com/jhowardmsft/opengcs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/github.com/jhowardmsft/opengcs/gogcs/client/config.go b/vendor/github.com/jhowardmsft/opengcs/gogcs/client/config.go new file mode 100644 index 0000000000..db4ccb6f6d --- /dev/null +++ b/vendor/github.com/jhowardmsft/opengcs/gogcs/client/config.go @@ -0,0 +1,234 @@ +// +build windows + +package client + +// TODO @jhowardmsft - This will move to Microsoft/opengcs soon + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strconv" + "strings" + + "github.com/Microsoft/hcsshim" + "github.com/Sirupsen/logrus" +) + +// Mode is the operational mode, both requested, and actual after verification +type Mode uint + +const ( + // Constants for the actual mode after validation + + // ModeActualError means an error has occurred during validation + ModeActualError = iota + // ModeActualVhdx means that we are going to use VHDX boot after validation + ModeActualVhdx + // ModeActualKernelInitrd means that we are going to use kernel+initrd for boot after validation + ModeActualKernelInitrd + + // Constants for the requested mode + + // ModeRequestAuto means auto-select the boot mode for a utility VM + ModeRequestAuto = iota // VHDX will be priority over kernel+initrd + // ModeRequestVhdx means request VHDX boot if possible + ModeRequestVhdx + // ModeRequestKernelInitrd means request Kernel+initrd boot if possible + ModeRequestKernelInitrd + + // defaultUvmTimeoutSeconds is the default time to wait for utility VM operations + defaultUvmTimeoutSeconds = 5 * 60 + + // DefaultSandboxSizeMB is the size of the default sandbox size in MB + DefaultSandboxSizeMB = 20 * 1024 * 1024 +) + +// Config is the structure used to configuring a utility VM to be used +// as a service VM. There are two ways of starting. Either supply a VHD, +// or a Kernel+Initrd. For the latter, both must be supplied, and both +// must be in the same directory. +// +// VHD is the priority. +// +// All paths are full host path-names. +type Config struct { + Kernel string // Kernel for Utility VM (embedded in a UEFI bootloader) + Initrd string // Initrd image for Utility VM + Vhdx string // VHD for booting the utility VM + Name string // Name of the utility VM + RequestedMode Mode // What mode is preferred when validating + ActualMode Mode // What mode was obtained during validation + UvmTimeoutSeconds int // How long to wait for the utility VM to respond in seconds + Uvm hcsshim.Container // The actual container +} + +// GenerateDefault generates a default config from a set of options +// If baseDir is not supplied, defaults to $env:ProgramFiles\lcow +func (config *Config) GenerateDefault(options []string) error { + baseDir := filepath.Join(os.Getenv("ProgramFiles"), "lcow") + + if _, err := os.Stat(baseDir); os.IsNotExist(err) { + return fmt.Errorf("opengcs: cannot create default utility VM configuration as directory '%s' was not found", baseDir) + } + + if config.UvmTimeoutSeconds < 0 { + return fmt.Errorf("opengcs: cannot generate a config when supplied a negative utility VM timeout") + } + + envTimeoutSeconds := 0 + optTimeoutSeconds := 0 + + if config.UvmTimeoutSeconds != 0 { + envTimeout := os.Getenv("OPENGCS_UVM_TIMEOUT_SECONDS") + if len(envTimeout) > 0 { + var err error + if envTimeoutSeconds, err = strconv.Atoi(envTimeout); err != nil { + return fmt.Errorf("opengcs: OPENGCS_UVM_TIMEOUT_SECONDS could not be interpreted as an integer") + } + if envTimeoutSeconds < 0 { + return fmt.Errorf("opengcs: OPENGCS_UVM_TIMEOUT_SECONDS cannot be negative") + } + } + } + + config.Vhdx = filepath.Join(baseDir, `uvm.vhdx`) + config.Kernel = filepath.Join(baseDir, `bootx64.efi`) + config.Initrd = filepath.Join(baseDir, `initrd.img`) + + for _, v := range options { + opt := strings.SplitN(v, "=", 2) + if len(opt) == 2 { + switch strings.ToLower(opt[0]) { + case "opengcskernel": + config.Kernel = opt[1] + case "opengcsinitrd": + config.Initrd = opt[1] + case "opengcsvhdx": + config.Vhdx = opt[1] + case "opengcstimeoutsecs": + var err error + if optTimeoutSeconds, err = strconv.Atoi(opt[1]); err != nil { + return fmt.Errorf("opengcs: opengcstimeoutsecs option could not be interpreted as an integer") + } + if optTimeoutSeconds < 0 { + return fmt.Errorf("opengcs: opengcstimeoutsecs option cannot be negative") + } + } + } + } + + // Which timeout are we going to take? If not through option or environment, + // then use the default constant, otherwise the maximum of the option or + // environment supplied setting. A requested on in the config supplied + // overrides all of this. + if config.UvmTimeoutSeconds == 0 { + config.UvmTimeoutSeconds = defaultUvmTimeoutSeconds + if optTimeoutSeconds != 0 || envTimeoutSeconds != 0 { + config.UvmTimeoutSeconds = optTimeoutSeconds + if envTimeoutSeconds > optTimeoutSeconds { + config.UvmTimeoutSeconds = envTimeoutSeconds + } + } + } + + return nil +} + +// validate validates a Config structure for starting a utility VM. +func (config *Config) validate() error { + config.ActualMode = ModeActualError + + if config.RequestedMode == ModeRequestVhdx && config.Vhdx == "" { + return fmt.Errorf("opengcs: config is invalid - request for VHDX mode did not supply a VHDX") + } + if config.RequestedMode == ModeRequestKernelInitrd && (config.Kernel == "" || config.Initrd == "") { + return fmt.Errorf("opengcs: config is invalid - request for Kernel+Initrd mode must supply both kernel and initrd") + } + + // Validate that if VHDX requested or auto, it exists. + if config.RequestedMode == ModeRequestAuto || config.RequestedMode == ModeRequestVhdx { + if _, err := os.Stat(config.Vhdx); os.IsNotExist(err) { + if config.RequestedMode == ModeRequestVhdx { + return fmt.Errorf("opengcs: mode requested was VHDX but '%s' could not be found", config.Vhdx) + } + } else { + config.ActualMode = ModeActualVhdx + return nil + } + } + + // So must be kernel+initrd, or auto where we fallback as the VHDX doesn't exist + if config.Initrd == "" || config.Kernel == "" { + if config.RequestedMode == ModeRequestKernelInitrd { + return fmt.Errorf("opengcs: both initrd and kernel options for utility VM boot must be supplied") + } + return fmt.Errorf("opengcs: configuration is invalid") + } + if _, err := os.Stat(config.Kernel); os.IsNotExist(err) { + return fmt.Errorf("opengcs: kernel '%s' was not found", config.Kernel) + } + if _, err := os.Stat(config.Initrd); os.IsNotExist(err) { + return fmt.Errorf("opengcs: initrd '%s' was not found", config.Initrd) + } + dk, _ := filepath.Split(config.Kernel) + di, _ := filepath.Split(config.Initrd) + if dk != di { + return fmt.Errorf("initrd '%s' and kernel '%s' must be located in the same directory", config.Initrd, config.Kernel) + } + + config.ActualMode = ModeActualKernelInitrd + return nil +} + +// Create creates a utility VM from a configuration. +func (config *Config) Create() error { + logrus.Debugf("opengcs Create: %+v", config) + + if err := config.validate(); err != nil { + return err + } + + configuration := &hcsshim.ContainerConfig{ + HvPartition: true, + Name: config.Name, + SystemType: "container", + ContainerType: "linux", + TerminateOnLastHandleClosed: true, + } + + if config.ActualMode == ModeActualVhdx { + configuration.HvRuntime = &hcsshim.HvRuntime{ + ImagePath: config.Vhdx, + } + } else { + // TODO @jhowardmsft - with a platform change that is in-flight, remove ImagePath for + // initrd/kernel boot. Current platform requires it. + dir, _ := filepath.Split(config.Initrd) + configuration.HvRuntime = &hcsshim.HvRuntime{ + ImagePath: dir, + LinuxInitrdPath: config.Initrd, + LinuxKernelPath: config.Kernel, + } + } + + configurationS, _ := json.Marshal(configuration) + logrus.Debugf("opengcs Create: calling HCS with '%s'", string(configurationS)) + uvm, err := hcsshim.CreateContainer(config.Name, configuration) + if err != nil { + return err + } + logrus.Debugf("opengcs Create: uvm created, starting...") + err = uvm.Start() + if err != nil { + logrus.Debugf("opengcs Create: uvm failed to start: %s", err) + // Make sure we don't leave it laying around as it's been created in HCS + uvm.Terminate() + return err + } + + config.Uvm = uvm + logrus.Debugf("opengcs Create: uvm %s is running", config.Name) + return nil +} diff --git a/vendor/github.com/jhowardmsft/opengcs/gogcs/client/createsandbox.go b/vendor/github.com/jhowardmsft/opengcs/gogcs/client/createsandbox.go new file mode 100644 index 0000000000..66ab703735 --- /dev/null +++ b/vendor/github.com/jhowardmsft/opengcs/gogcs/client/createsandbox.go @@ -0,0 +1,78 @@ +// +build windows + +package client + +// TODO @jhowardmsft - This will move to Microsoft/opengcs soon + +import ( + "fmt" + "os" + "sync" + + "github.com/Sirupsen/logrus" +) + +var sandboxCacheLock sync.Mutex + +// CreateSandbox does what it says on the tin. This is done by copying a prebuilt-sandbox from the ServiceVM +// TODO: @jhowardmsft maxSizeInMB isn't hooked up in GCS. Needs a platform change which is in flight. +func (config *Config) CreateSandbox(destFile string, maxSizeInMB uint32, cacheFile string) error { + // Smallest we can accept is the default sandbox size as we can't size down, only expand. + if maxSizeInMB < DefaultSandboxSizeMB { + maxSizeInMB = DefaultSandboxSizeMB + } + + logrus.Debugf("opengcs: CreateSandbox: %s size:%dMB cache:%s", destFile, maxSizeInMB, cacheFile) + + // Retrieve from cache if the default size and already on disk + if maxSizeInMB == DefaultSandboxSizeMB { + sandboxCacheLock.Lock() + if _, err := os.Stat(cacheFile); err == nil { + if err := copyFile(cacheFile, destFile); err != nil { + sandboxCacheLock.Unlock() + return fmt.Errorf("opengcs: CreateSandbox: Failed to copy cached sandbox '%s' to '%s': %s", cacheFile, destFile, err) + } + sandboxCacheLock.Unlock() + logrus.Debugf("opengcs: CreateSandbox: %s fulfilled from cache", destFile) + return nil + } + sandboxCacheLock.Unlock() + } + + if config.Uvm == nil { + return fmt.Errorf("opengcs: CreateSandbox: No utility VM has been created") + } + + // TODO @jhowardmsft - needs a platform change so that can specify size. eg fmt.Sprintf("createSandbox -size %d", maxSizeInMB)) + process, err := config.createUtilsProcess("createSandbox") + if err != nil { + return fmt.Errorf("opengcs: CreateSandbox: %s: failed to create utils process: %s", destFile, err) + } + + defer func() { + process.Process.Close() + }() + + logrus.Debugf("opengcs: CreateSandbox: %s: writing from stdout", destFile) + // Get back the sandbox VHDx stream from the service VM and write it to file + resultSize, err := writeFileFromReader(destFile, process.Stdout, config.UvmTimeoutSeconds, fmt.Sprintf("createSandbox %s", destFile)) + if err != nil { + return fmt.Errorf("opengcs: CreateSandbox: %s: failed writing %d bytes to target file: %s", destFile, resultSize, err) + } + + // Populate the cache + if maxSizeInMB == DefaultSandboxSizeMB { + sandboxCacheLock.Lock() + // It may already exist due to being created on another thread, in which case no copy back needed. + if _, err := os.Stat(cacheFile); os.IsNotExist(err) { + if err := copyFile(destFile, cacheFile); err != nil { + sandboxCacheLock.Unlock() + return fmt.Errorf("opengcs: CreateSandbox: Failed to seed sandbox cache '%s' from '%s': %s", destFile, cacheFile, err) + } + } + sandboxCacheLock.Unlock() + } + + logrus.Debugf("opengcs: CreateSandbox: %s created (non-cache)", destFile) + return nil +} diff --git a/vendor/github.com/jhowardmsft/opengcs/gogcs/client/hotaddvhd.go b/vendor/github.com/jhowardmsft/opengcs/gogcs/client/hotaddvhd.go new file mode 100644 index 0000000000..62de79016a --- /dev/null +++ b/vendor/github.com/jhowardmsft/opengcs/gogcs/client/hotaddvhd.go @@ -0,0 +1,41 @@ +// +build windows + +package client + +// TODO @jhowardmsft - This will move to Microsoft/opengcs soon + +import ( + "fmt" + + "github.com/Microsoft/hcsshim" + "github.com/Sirupsen/logrus" +) + +// HotAddVhd hot-adds a VHD to a utility VM. This is used in the global one-utility-VM- +// service-VM per host scenario. In order to do a graphdriver `Diff`, we hot-add the +// sandbox to /mnt/ so that we can run `exportSandbox` inside the utility VM to +// get a tar-stream of the sandboxes contents back to the daemon. +func (config *Config) HotAddVhd(hostPath string, containerPath string) error { + logrus.Debugf("opengcs: HotAddVhd: %s: %s", hostPath, containerPath) + + if config.Uvm == nil { + return fmt.Errorf("cannot hot-add VHD as no utility VM is in configuration") + } + + modification := &hcsshim.ResourceModificationRequestResponse{ + Resource: "MappedVirtualDisk", + Data: hcsshim.MappedVirtualDisk{ + HostPath: hostPath, + ContainerPath: containerPath, + CreateInUtilityVM: true, + //ReadOnly: true, + }, + Request: "Add", + } + logrus.Debugf("opengcs: HotAddVhd: %s to %s", hostPath, containerPath) + if err := config.Uvm.Modify(modification); err != nil { + return fmt.Errorf("opengcs: HotAddVhd: failed: %s", err) + } + logrus.Debugf("opengcs: HotAddVhd: %s added successfully", hostPath) + return nil +} diff --git a/vendor/github.com/jhowardmsft/opengcs/gogcs/client/hotremovevhd.go b/vendor/github.com/jhowardmsft/opengcs/gogcs/client/hotremovevhd.go new file mode 100644 index 0000000000..71167d00ee --- /dev/null +++ b/vendor/github.com/jhowardmsft/opengcs/gogcs/client/hotremovevhd.go @@ -0,0 +1,36 @@ +// +build windows + +package client + +// TODO @jhowardmsft - This will move to Microsoft/opengcs soon + +import ( + "fmt" + + "github.com/Microsoft/hcsshim" + "github.com/Sirupsen/logrus" +) + +// HotRemoveVhd hot-removes a VHD from a utility VM. This is used in the global one-utility-VM- +// service-VM per host scenario. +func (config *Config) HotRemoveVhd(hostPath string) error { + logrus.Debugf("opengcs: HotRemoveVhd: %s", hostPath) + + if config.Uvm == nil { + return fmt.Errorf("cannot hot-add VHD as no utility VM is in configuration") + } + + modification := &hcsshim.ResourceModificationRequestResponse{ + Resource: "MappedVirtualDisk", + Data: hcsshim.MappedVirtualDisk{ + HostPath: hostPath, + CreateInUtilityVM: true, + }, + Request: "Remove", + } + if err := config.Uvm.Modify(modification); err != nil { + return fmt.Errorf("opengcs: HotRemoveVhd: %s failed: %s", hostPath, err) + } + logrus.Debugf("opengcs: HotRemoveVhd: %s removed successfully", hostPath) + return nil +} diff --git a/vendor/github.com/jhowardmsft/opengcs/gogcs/client/layervhddetails.go b/vendor/github.com/jhowardmsft/opengcs/gogcs/client/layervhddetails.go new file mode 100644 index 0000000000..6ce29cdc56 --- /dev/null +++ b/vendor/github.com/jhowardmsft/opengcs/gogcs/client/layervhddetails.go @@ -0,0 +1,33 @@ +// +build windows + +package client + +// TODO @jhowardmsft - This will move to Microsoft/opengcs soon + +import ( + "fmt" + "os" + "path/filepath" +) + +// LayerVhdDetails is a utility for getting a file name, size and indication of +// sandbox for a VHD(x) in a folder. A read-only layer will be layer.vhd. A +// read-write layer will be sandbox.vhdx. +func LayerVhdDetails(folder string) (string, int64, bool, error) { + var fileInfo os.FileInfo + isSandbox := false + filename := filepath.Join(folder, "layer.vhd") + var err error + + if fileInfo, err = os.Stat(filename); err != nil { + filename = filepath.Join(folder, "sandbox.vhdx") + if fileInfo, err = os.Stat(filename); err != nil { + if os.IsNotExist(err) { + return "", 0, isSandbox, fmt.Errorf("could not find layer or sandbox in %s", folder) + } + return "", 0, isSandbox, fmt.Errorf("error locating layer or sandbox in %s: %s", folder, err) + } + isSandbox = true + } + return filename, fileInfo.Size(), isSandbox, nil +} diff --git a/vendor/github.com/jhowardmsft/opengcs/gogcs/client/process.go b/vendor/github.com/jhowardmsft/opengcs/gogcs/client/process.go new file mode 100644 index 0000000000..6592ab6a03 --- /dev/null +++ b/vendor/github.com/jhowardmsft/opengcs/gogcs/client/process.go @@ -0,0 +1,61 @@ +// +build windows + +package client + +// TODO @jhowardmsft - This will move to Microsoft/opengcs soon + +import ( + "fmt" + "io" + + "github.com/Microsoft/hcsshim" + "github.com/Sirupsen/logrus" +) + +// Process is the structure pertaining to a process running in a utility VM. +type process struct { + Process hcsshim.Process + Stdin io.WriteCloser + Stdout io.ReadCloser +} + +// createUtilsProcess is a convenient wrapper for hcsshim.createUtilsProcess to use when +// communicating with a utility VM. +func (config *Config) createUtilsProcess(commandLine string) (process, error) { + logrus.Debugf("opengcs: createUtilsProcess") + + if config.Uvm == nil { + return process{}, fmt.Errorf("cannot create utils process as no utility VM is in configuration") + } + + var ( + err error + proc process + ) + + env := make(map[string]string) + env["PATH"] = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:" + processConfig := &hcsshim.ProcessConfig{ + EmulateConsole: false, + CreateStdInPipe: true, + CreateStdOutPipe: true, + CreateStdErrPipe: true, + CreateInUtilityVm: true, + WorkingDirectory: "/bin", + Environment: env, + CommandLine: commandLine, + } + proc.Process, err = config.Uvm.CreateProcess(processConfig) + if err != nil { + return process{}, fmt.Errorf("opengcs: createUtilsProcess: CreateProcess %+v failed %s", config, err) + } + + if proc.Stdin, proc.Stdout, _, err = proc.Process.Stdio(); err != nil { + proc.Process.Kill() // Should this have a timeout? + proc.Process.Close() + return process{}, fmt.Errorf("opengcs: createUtilsProcess: failed to get Stdio pipes %s", err) + } + + logrus.Debugf("opengcs: createUtilsProcess success: pid %d", proc.Process.Pid()) + return proc, nil +} diff --git a/vendor/github.com/jhowardmsft/opengcs/gogcs/client/tartovhd.go b/vendor/github.com/jhowardmsft/opengcs/gogcs/client/tartovhd.go new file mode 100644 index 0000000000..8832e096e6 --- /dev/null +++ b/vendor/github.com/jhowardmsft/opengcs/gogcs/client/tartovhd.go @@ -0,0 +1,44 @@ +// +build windows + +package client + +// TODO @jhowardmsft - This will move to Microsoft/opengcs soon + +import ( + "fmt" + "io" + + "github.com/Sirupsen/logrus" +) + +// TarToVhd streams a tarstream contained in an io.Reader to a fixed vhd file +func (config *Config) TarToVhd(targetVHDFile string, reader io.Reader) (int64, error) { + logrus.Debugf("opengcs: TarToVhd: %s", targetVHDFile) + + if config.Uvm == nil { + return 0, fmt.Errorf("cannot Tar2Vhd as no utility VM is in configuration") + } + + process, err := config.createUtilsProcess("tar2vhd") + if err != nil { + return 0, fmt.Errorf("opengcs: TarToVhd: %s: failed to create utils process tar2vhd: %s", targetVHDFile, err) + } + defer process.Process.Close() + + // Send the tarstream into the `tar2vhd`s stdin + if _, err = copyWithTimeout(process.Stdin, reader, 0, config.UvmTimeoutSeconds, fmt.Sprintf("send %s, to stdin of tar2vhd", targetVHDFile)); err != nil { + return 0, fmt.Errorf("opengcs: TarToVhd: %s: failed to send to tar2vhd in uvm: %s", targetVHDFile, err) + } + + // Don't need stdin now we've sent everything. This signals GCS that we are finished sending data. + process.Process.CloseStdin() + + // Write stdout contents of `tar2vhd` to the VHD file + payloadSize, err := writeFileFromReader(targetVHDFile, process.Stdout, config.UvmTimeoutSeconds, fmt.Sprintf("output of tar2vhd to %s", targetVHDFile)) + if err != nil { + return 0, fmt.Errorf("opengcs: TarToVhd: %s: failed writing VHD file: %s", targetVHDFile, err) + } + + logrus.Debugf("opengcs: TarToVhd: %s created, %d bytes", targetVHDFile, payloadSize) + return payloadSize, err +} diff --git a/vendor/github.com/jhowardmsft/opengcs/gogcs/client/unsupported.go b/vendor/github.com/jhowardmsft/opengcs/gogcs/client/unsupported.go new file mode 100644 index 0000000000..4a7abab3b0 --- /dev/null +++ b/vendor/github.com/jhowardmsft/opengcs/gogcs/client/unsupported.go @@ -0,0 +1,5 @@ +// +build !windows + +package client + +// TODO @jhowardmsft - This will move to Microsoft/opengcs soon diff --git a/vendor/github.com/jhowardmsft/opengcs/gogcs/client/utilities.go b/vendor/github.com/jhowardmsft/opengcs/gogcs/client/utilities.go new file mode 100644 index 0000000000..427cde7795 --- /dev/null +++ b/vendor/github.com/jhowardmsft/opengcs/gogcs/client/utilities.go @@ -0,0 +1,110 @@ +// +build windows + +package client + +// TODO @jhowardmsft - This will move to Microsoft/opengcs soon + +import ( + "fmt" + "io" + "os" + "syscall" + "time" + "unsafe" + + "github.com/Sirupsen/logrus" +) + +var ( + modkernel32 = syscall.NewLazyDLL("kernel32.dll") + procCopyFileW = modkernel32.NewProc("CopyFileW") +) + +// writeFileFromReader writes an output file from an io.Reader +func writeFileFromReader(path string, reader io.Reader, timeoutSeconds int, context string) (int64, error) { + outFile, err := os.Create(path) + if err != nil { + return 0, fmt.Errorf("opengcs: writeFileFromReader: failed to create %s: %s", path, err) + } + defer outFile.Close() + return copyWithTimeout(outFile, reader, 0, timeoutSeconds, context) +} + +// copyWithTimeout is a wrapper for io.Copy using a timeout duration +func copyWithTimeout(dst io.Writer, src io.Reader, size int64, timeoutSeconds int, context string) (int64, error) { + logrus.Debugf("opengcs: copywithtimeout: size %d: timeout %d: (%s)", size, timeoutSeconds, context) + + type resultType struct { + err error + bytes int64 + } + + done := make(chan resultType, 1) + go func() { + // TODO @jhowardmsft. Needs platform fix. Improve reliability by + // chunking the data. Ultimately can just use io.Copy instead with no loop + result := resultType{} + var copied int64 + for { + copied, result.err = io.CopyN(dst, src, 1024) + result.bytes += copied + if copied == 0 { + done <- result + break + } + // TODO @jhowardmsft - next line is debugging only. Remove + //logrus.Debugf("%s: copied so far %d\n", context, result.bytes) + } + }() + + var result resultType + timedout := time.After(time.Duration(timeoutSeconds) * time.Second) + + select { + case <-timedout: + return 0, fmt.Errorf("opengcs: copyWithTimeout: timed out (%s)", context) + case result = <-done: + if result.err != nil && result.err != io.EOF { + // See https://github.com/golang/go/blob/f3f29d1dea525f48995c1693c609f5e67c046893/src/os/exec/exec_windows.go for a clue as to why we are doing this :) + if se, ok := result.err.(syscall.Errno); ok { + const ( + errNoData = syscall.Errno(232) + errBrokenPipe = syscall.Errno(109) + ) + if se == errNoData || se == errBrokenPipe { + logrus.Debugf("opengcs: copyWithTimeout: hit NoData or BrokenPipe: %d: %s", se, context) + return result.bytes, nil + } + } + return 0, fmt.Errorf("opengcs: copyWithTimeout: error reading: '%s' after %d bytes (%s)", result.err, result.bytes, context) + } + } + logrus.Debugf("opengcs: copyWithTimeout: success - copied %d bytes (%s)", result.bytes, context) + return result.bytes, nil +} + +// copyFile is a utility for copying a file - used for the sandbox cache. +// Uses CopyFileW win32 API for performance +func copyFile(srcFile, destFile string) error { + var bFailIfExists uint32 = 1 + + lpExistingFileName, err := syscall.UTF16PtrFromString(srcFile) + if err != nil { + return err + } + lpNewFileName, err := syscall.UTF16PtrFromString(destFile) + if err != nil { + return err + } + r1, _, err := syscall.Syscall( + procCopyFileW.Addr(), + 3, + uintptr(unsafe.Pointer(lpExistingFileName)), + uintptr(unsafe.Pointer(lpNewFileName)), + uintptr(bFailIfExists)) + if r1 == 0 { + return fmt.Errorf("failed CopyFileW Win32 call from '%s' to %s: %s", srcFile, destFile, err) + } + return nil + +} diff --git a/vendor/github.com/jhowardmsft/opengcs/gogcs/client/vhdtotar.go b/vendor/github.com/jhowardmsft/opengcs/gogcs/client/vhdtotar.go new file mode 100644 index 0000000000..9504fb7c84 --- /dev/null +++ b/vendor/github.com/jhowardmsft/opengcs/gogcs/client/vhdtotar.go @@ -0,0 +1,69 @@ +// +build windows + +package client + +// TODO @jhowardmsft - This will move to Microsoft/opengcs soon + +import ( + "fmt" + "io" + "os" + + "github.com/Sirupsen/logrus" +) + +// VhdToTar does what is says - it exports a VHD in a specified +// folder (either a read-only layer.vhd, or a read-write sandbox.vhd) to a +// ReadCloser containing a tar-stream of the layers contents. +func (config *Config) VhdToTar(vhdFile string, uvmMountPath string, isSandbox bool, vhdSize int64) (io.ReadCloser, error) { + logrus.Debugf("opengcs: VhdToTar: %s isSandbox: %t", vhdFile, isSandbox) + + if config.Uvm == nil { + return nil, fmt.Errorf("cannot VhdToTar as no utility VM is in configuration") + } + + vhdHandle, err := os.Open(vhdFile) + if err != nil { + return nil, fmt.Errorf("opengcs: VhdToTar: failed to open %s: %s", vhdFile, err) + } + defer vhdHandle.Close() + logrus.Debugf("opengcs: VhdToTar: exporting %s, size %d, isSandbox %t", vhdHandle.Name(), vhdSize, isSandbox) + + // Different binary depending on whether a RO layer or a RW sandbox + command := "vhd2tar" + if isSandbox { + command = fmt.Sprintf("exportSandbox -path %s", uvmMountPath) + } + + // Start the binary in the utility VM + process, err := config.createUtilsProcess(command) + if err != nil { + return nil, fmt.Errorf("opengcs: VhdToTar: %s: failed to create utils process %s: %s", vhdHandle.Name(), command, err) + } + + if !isSandbox { + // Send the VHD contents to the utility VM processes stdin handle if not a sandbox + logrus.Debugf("opengcs: VhdToTar: copying the layer VHD into the utility VM") + if _, err = copyWithTimeout(process.Stdin, vhdHandle, vhdSize, config.UvmTimeoutSeconds, fmt.Sprintf("vhdtotarstream: sending %s to %s", vhdHandle.Name(), command)); err != nil { + process.Process.Close() + return nil, fmt.Errorf("opengcs: VhdToTar: %s: failed to copyWithTimeout on the stdin pipe (to utility VM): %s", vhdHandle.Name(), err) + } + } + + // Start a goroutine which copies the stdout (ie the tar stream) + reader, writer := io.Pipe() + go func() { + defer writer.Close() + defer process.Process.Close() + logrus.Debugf("opengcs: VhdToTar: copying tar stream back from the utility VM") + bytes, err := copyWithTimeout(writer, process.Stdout, vhdSize, config.UvmTimeoutSeconds, fmt.Sprintf("vhdtotarstream: copy tarstream from %s", command)) + if err != nil { + logrus.Errorf("opengcs: VhdToTar: %s: copyWithTimeout on the stdout pipe (from utility VM) failed: %s", vhdHandle.Name(), err) + } + logrus.Debugf("opengcs: VhdToTar: copied %d bytes of the tarstream of %s from the utility VM", bytes, vhdHandle.Name()) + }() + + // Return the read-side of the pipe connected to the goroutine which is reading from the stdout of the process in the utility VM + return reader, nil + +}