diff --git a/api/server/httputils/httputils.go b/api/server/httputils/httputils.go index caac9bbc7a..57eeb4beba 100644 --- a/api/server/httputils/httputils.go +++ b/api/server/httputils/httputils.go @@ -1,17 +1,11 @@ package httputils import ( - "encoding/json" - "fmt" "io" "mime" "net/http" - "runtime" "strings" - "github.com/docker/docker/api/types/versions" - "github.com/docker/docker/pkg/system" - specs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" "github.com/sirupsen/logrus" "golang.org/x/net/context" @@ -115,27 +109,3 @@ func matchesContentType(contentType, expectedType string) bool { } return err == nil && mimetype == expectedType } - -// GetRequestedPlatform extracts an optional platform structure from an HTTP request header -func GetRequestedPlatform(ctx context.Context, r *http.Request) (*specs.Platform, error) { - platform := &specs.Platform{} - version := VersionFromContext(ctx) - if versions.GreaterThanOrEqualTo(version, "1.32") { - requestedPlatform := r.Header.Get("X-Requested-Platform") - if requestedPlatform != "" { - if err := json.Unmarshal([]byte(requestedPlatform), platform); err != nil { - return nil, fmt.Errorf("invalid X-Requested-Platform header: %s", err) - } - } - if err := system.ValidatePlatform(platform); err != nil { - return nil, err - } - } - if platform.OS == "" { - platform.OS = runtime.GOOS - } - if platform.Architecture == "" { - platform.Architecture = runtime.GOARCH - } - return platform, nil -} diff --git a/api/server/router/build/build_routes.go b/api/server/router/build/build_routes.go index c637897184..fefb875de1 100644 --- a/api/server/router/build/build_routes.go +++ b/api/server/router/build/build_routes.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "os" "runtime" "strconv" "strings" @@ -20,6 +21,7 @@ import ( "github.com/docker/docker/pkg/ioutils" "github.com/docker/docker/pkg/progress" "github.com/docker/docker/pkg/streamformatter" + "github.com/docker/docker/pkg/system" units "github.com/docker/go-units" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -67,6 +69,24 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui options.Squash = httputils.BoolValue(r, "squash") options.Target = r.FormValue("target") options.RemoteContext = r.FormValue("remote") + if versions.GreaterThanOrEqualTo(version, "1.32") { + // TODO @jhowardmsft. The following environment variable is an interim + // measure to allow the daemon to have a default platform if omitted by + // the client. This allows LCOW and WCOW to work with a down-level CLI + // for a short period of time, as the CLI changes can't be merged + // until after the daemon changes have been merged. Once the CLI is + // updated, this can be removed. PR for CLI is currently in + // https://github.com/docker/cli/pull/474. + apiPlatform := r.FormValue("platform") + if system.LCOWSupported() && apiPlatform == "" { + apiPlatform = os.Getenv("LCOW_API_PLATFORM_IF_OMITTED") + } + p := system.ParsePlatform(apiPlatform) + if err := system.ValidatePlatform(p); err != nil { + return nil, validationError{fmt.Errorf("invalid platform: %s", err)} + } + options.Platform = p.OS + } if r.Form.Get("shmsize") != "" { shmSize, err := strconv.ParseInt(r.Form.Get("shmsize"), 10, 64) @@ -87,12 +107,6 @@ func newImageBuildOptions(ctx context.Context, r *http.Request) (*types.ImageBui return nil, validationError{fmt.Errorf("The daemon on this platform does not support setting security options on build")} } - platform, err := httputils.GetRequestedPlatform(ctx, r) - if err != nil { - return nil, err - } - options.Platform = *platform - var buildUlimits = []*units.Ulimit{} ulimitsJSON := r.FormValue("ulimits") if ulimitsJSON != "" { diff --git a/api/server/router/image/image_routes.go b/api/server/router/image/image_routes.go index 744a796a68..dabab3bcf9 100644 --- a/api/server/router/image/image_routes.go +++ b/api/server/router/image/image_routes.go @@ -3,8 +3,10 @@ package image import ( "encoding/base64" "encoding/json" + "fmt" "io" "net/http" + "os" "strconv" "strings" @@ -16,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" specs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" @@ -70,6 +73,7 @@ func (s *imageRouter) postCommit(ctx context.Context, w http.ResponseWriter, r * // Creates an image from Pull or from Import func (s *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWriter, r *http.Request, vars map[string]string) error { + if err := httputils.ParseForm(r); err != nil { return err } @@ -87,7 +91,25 @@ func (s *imageRouter) postImagesCreate(ctx context.Context, w http.ResponseWrite w.Header().Set("Content-Type", "application/json") - platform, err = httputils.GetRequestedPlatform(ctx, r) + version := httputils.VersionFromContext(ctx) + if versions.GreaterThanOrEqualTo(version, "1.32") { + // TODO @jhowardmsft. The following environment variable is an interim + // measure to allow the daemon to have a default platform if omitted by + // the client. This allows LCOW and WCOW to work with a down-level CLI + // for a short period of time, as the CLI changes can't be merged + // until after the daemon changes have been merged. Once the CLI is + // updated, this can be removed. PR for CLI is currently in + // https://github.com/docker/cli/pull/474. + apiPlatform := r.FormValue("platform") + if system.LCOWSupported() && apiPlatform == "" { + apiPlatform = os.Getenv("LCOW_API_PLATFORM_IF_OMITTED") + } + platform = system.ParsePlatform(apiPlatform) + if err = system.ValidatePlatform(platform); err != nil { + err = fmt.Errorf("invalid platform: %s", err) + } + } + if err == nil { if image != "" { //pull metaHeaders := map[string][]string{} diff --git a/api/swagger.yaml b/api/swagger.yaml index a68c83b74e..2e4615f77f 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -6181,17 +6181,9 @@ paths: Only the registry domain name (and port if not the default 443) are required. However, for legacy reasons, the Docker Hub registry must be specified with both a `https://` prefix and a `/v1/` suffix even though Docker will prefer to use the v2 registry API. type: "string" - - name: "X-Requested-Platform" - in: "header" - description: | - This is a JSON object representing an OCI image-spec `Platform` object. It is used to request a platform in the case that the engine supports multiple platforms. For example: - - ``` - { - "architecture": "amd64", - "os": "linux" - } - ``` + - name: "platform" + in: "query" + description: "Platform in the format os[/arch[/variant]]" type: "string" default: "" responses: @@ -6275,17 +6267,9 @@ paths: in: "header" description: "A base64-encoded auth configuration. [See the authentication section for details.](#section/Authentication)" type: "string" - - name: "X-Requested-Platform" - in: "header" - description: | - This is a JSON object representing an OCI image-spec `Platform` object. It is used to request a platform in the case that the engine supports multiple platforms. For example: - - ``` - { - "architecture": "amd64", - "os": "linux" - } - ``` + - name: "platform" + in: "query" + description: "Platform in the format os[/arch[/variant]]" type: "string" default: "" tags: ["Image"] diff --git a/api/types/client.go b/api/types/client.go index 656b14dd3e..db37f1fe4e 100644 --- a/api/types/client.go +++ b/api/types/client.go @@ -8,7 +8,6 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/filters" units "github.com/docker/go-units" - specs "github.com/opencontainers/image-spec/specs-go/v1" ) // CheckpointCreateOptions holds parameters to create a checkpoint from a container @@ -180,7 +179,7 @@ type ImageBuildOptions struct { ExtraHosts []string // List of extra hosts Target string SessionID string - Platform specs.Platform + Platform string } // ImageBuildResponse holds information @@ -193,8 +192,8 @@ type ImageBuildResponse struct { // ImageCreateOptions holds information to create images. type ImageCreateOptions struct { - RegistryAuth string // RegistryAuth is the base64 encoded credentials for the registry. - Platform specs.Platform // Platform is the target platform of the image if it needs to be pulled from the registry. + RegistryAuth string // RegistryAuth is the base64 encoded credentials for the registry. + Platform string // Platform is the target platform of the image if it needs to be pulled from the registry. } // ImageImportSource holds source information for ImageImport @@ -205,9 +204,10 @@ type ImageImportSource struct { // ImageImportOptions holds information to import images from the client host. type ImageImportOptions struct { - Tag string // Tag is the name to tag this image with. This attribute is deprecated. - Message string // Message is the message to tag the image with - Changes []string // Changes are the raw changes to apply to this image + Tag string // Tag is the name to tag this image with. This attribute is deprecated. + Message string // Message is the message to tag the image with + Changes []string // Changes are the raw changes to apply to this image + Platform string // Platform is the target platform of the image } // ImageListOptions holds parameters to filter the list of images with. @@ -228,7 +228,7 @@ type ImagePullOptions struct { All bool RegistryAuth string // RegistryAuth is the base64 encoded credentials for the registry PrivilegeFunc RequestPrivilegeFunc - Platform specs.Platform + Platform string } // RequestPrivilegeFunc is a function interface that diff --git a/api/types/types.go b/api/types/types.go index 14f908157b..dbce05bf32 100644 --- a/api/types/types.go +++ b/api/types/types.go @@ -15,6 +15,7 @@ import ( "github.com/docker/docker/api/types/registry" "github.com/docker/docker/api/types/swarm" "github.com/docker/go-connections/nat" + specs "github.com/opencontainers/image-spec/specs-go/v1" ) // RootFS returns Image's RootFS description including the layer IDs. @@ -327,7 +328,7 @@ type ContainerJSONBase struct { Name string RestartCount int Driver string - OS string + Platform specs.Platform MountLabel string ProcessLabel string AppArmorProfile string diff --git a/builder/dockerfile/builder.go b/builder/dockerfile/builder.go index 7ab9ba42f8..a0610437c8 100644 --- a/builder/dockerfile/builder.go +++ b/builder/dockerfile/builder.go @@ -20,6 +20,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/moby/buildkit/session" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -102,15 +103,16 @@ func (bm *BuildManager) Build(ctx context.Context, config backend.BuildConfig) ( } os := runtime.GOOS + optionsPlatform := system.ParsePlatform(config.Options.Platform) if dockerfile.OS != "" { - if config.Options.Platform.OS != "" && config.Options.Platform.OS != dockerfile.OS { + if optionsPlatform.OS != "" && optionsPlatform.OS != dockerfile.OS { return nil, fmt.Errorf("invalid platform") } os = dockerfile.OS - } else if config.Options.Platform.OS != "" { - os = config.Options.Platform.OS + } else if optionsPlatform.OS != "" { + os = optionsPlatform.OS } - config.Options.Platform.OS = os + config.Options.Platform = os dockerfile.OS = os builderOptions := builderOptions{ diff --git a/builder/dockerfile/copy.go b/builder/dockerfile/copy.go index 16cc5a16a2..92ba0069dd 100644 --- a/builder/dockerfile/copy.go +++ b/builder/dockerfile/copy.go @@ -82,7 +82,7 @@ func copierFromDispatchRequest(req dispatchRequest, download sourceDownloader, i pathCache: req.builder.pathCache, download: download, imageSource: imageSource, - platform: req.builder.options.Platform.OS, + platform: req.builder.options.Platform, } } diff --git a/builder/dockerfile/dispatchers.go b/builder/dockerfile/dispatchers.go index 3039a0db2f..8d3ea7ed62 100644 --- a/builder/dockerfile/dispatchers.go +++ b/builder/dockerfile/dispatchers.go @@ -194,11 +194,6 @@ func dispatchTriggeredOnBuild(d dispatchRequest, triggers []string) error { return nil } -// scratchImage is used as a token for the empty base image. It uses buildStage -// as a convenient implementation of builder.Image, but is not actually a -// buildStage. -var scratchImage builder.Image = &image.Image{} - func (d *dispatchRequest) getExpandedImageName(shlex *ShellLex, name string) (string, error) { substitutionArgs := []string{} for key, value := range d.state.buildArgs.GetAllMeta() { @@ -223,8 +218,9 @@ func (d *dispatchRequest) getImageOrStage(name string) (builder.Image, error) { imageImage := &image.Image{} imageImage.OS = runtime.GOOS if runtime.GOOS == "windows" { - switch d.builder.options.Platform.OS { - case "windows": + optionsOS := system.ParsePlatform(d.builder.options.Platform).OS + switch optionsOS { + case "windows", "": return nil, errors.New("Windows does not support FROM scratch") case "linux": if !system.LCOWSupported() { @@ -232,7 +228,7 @@ func (d *dispatchRequest) getImageOrStage(name string) (builder.Image, error) { } imageImage.OS = "linux" default: - return nil, errors.Errorf("operating system %q is not supported", d.builder.options.Platform.OS) + return nil, errors.Errorf("operating system %q is not supported", optionsOS) } } return builder.Image(imageImage), nil @@ -264,7 +260,8 @@ func dispatchOnbuild(d dispatchRequest, c *instructions.OnbuildCommand) error { func dispatchWorkdir(d dispatchRequest, c *instructions.WorkdirCommand) error { runConfig := d.state.runConfig var err error - runConfig.WorkingDir, err = normalizeWorkdir(d.builder.options.Platform.OS, runConfig.WorkingDir, c.Path) + optionsOS := system.ParsePlatform(d.builder.options.Platform).OS + runConfig.WorkingDir, err = normalizeWorkdir(optionsOS, runConfig.WorkingDir, c.Path) if err != nil { return err } @@ -280,7 +277,7 @@ func dispatchWorkdir(d dispatchRequest, c *instructions.WorkdirCommand) error { } comment := "WORKDIR " + runConfig.WorkingDir - runConfigWithCommentCmd := copyRunConfig(runConfig, withCmdCommentString(comment, d.builder.options.Platform.OS)) + runConfigWithCommentCmd := copyRunConfig(runConfig, withCmdCommentString(comment, optionsOS)) containerID, err := d.builder.probeAndCreate(d.state, runConfigWithCommentCmd) if err != nil || containerID == "" { return err @@ -313,7 +310,8 @@ func resolveCmdLine(cmd instructions.ShellDependantCmdLine, runConfig *container func dispatchRun(d dispatchRequest, c *instructions.RunCommand) error { stateRunConfig := d.state.runConfig - cmdFromArgs := resolveCmdLine(c.ShellDependantCmdLine, stateRunConfig, d.builder.options.Platform.OS) + optionsOS := system.ParsePlatform(d.builder.options.Platform).OS + cmdFromArgs := resolveCmdLine(c.ShellDependantCmdLine, stateRunConfig, optionsOS) buildArgs := d.state.buildArgs.FilterAllowed(stateRunConfig.Env) saveCmd := cmdFromArgs @@ -390,7 +388,8 @@ func prependEnvOnCmd(buildArgs *buildArgs, buildArgVars []string, cmd strslice.S // func dispatchCmd(d dispatchRequest, c *instructions.CmdCommand) error { runConfig := d.state.runConfig - cmd := resolveCmdLine(c.ShellDependantCmdLine, runConfig, d.builder.options.Platform.OS) + optionsOS := system.ParsePlatform(d.builder.options.Platform).OS + cmd := resolveCmdLine(c.ShellDependantCmdLine, runConfig, optionsOS) runConfig.Cmd = cmd // set config as already being escaped, this prevents double escaping on windows runConfig.ArgsEscaped = true @@ -433,7 +432,8 @@ func dispatchHealthcheck(d dispatchRequest, c *instructions.HealthCheckCommand) // func dispatchEntrypoint(d dispatchRequest, c *instructions.EntrypointCommand) error { runConfig := d.state.runConfig - cmd := resolveCmdLine(c.ShellDependantCmdLine, runConfig, d.builder.options.Platform.OS) + optionsOS := system.ParsePlatform(d.builder.options.Platform).OS + cmd := resolveCmdLine(c.ShellDependantCmdLine, runConfig, optionsOS) runConfig.Entrypoint = cmd if !d.state.cmdSet { runConfig.Cmd = nil diff --git a/builder/dockerfile/dispatchers_test.go b/builder/dockerfile/dispatchers_test.go index 3c1220cbbe..dc9148bca5 100644 --- a/builder/dockerfile/dispatchers_test.go +++ b/builder/dockerfile/dispatchers_test.go @@ -14,7 +14,6 @@ import ( "github.com/docker/docker/builder/dockerfile/instructions" "github.com/docker/docker/pkg/system" "github.com/docker/go-connections/nat" - specs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -23,13 +22,13 @@ func newBuilderWithMockBackend() *Builder { mockBackend := &MockBackend{} ctx := context.Background() b := &Builder{ - options: &types.ImageBuildOptions{Platform: specs.Platform{OS: runtime.GOOS}}, + options: &types.ImageBuildOptions{Platform: runtime.GOOS}, docker: mockBackend, Stdout: new(bytes.Buffer), clientCtx: ctx, disableCommit: true, imageSources: newImageSources(ctx, builderOptions{ - Options: &types.ImageBuildOptions{Platform: specs.Platform{OS: runtime.GOOS}}, + Options: &types.ImageBuildOptions{Platform: runtime.GOOS}, Backend: mockBackend, }), imageProber: newImageProber(mockBackend, nil, runtime.GOOS, false), diff --git a/builder/dockerfile/evaluator.go b/builder/dockerfile/evaluator.go index 50116b2c62..0841bc7924 100644 --- a/builder/dockerfile/evaluator.go +++ b/builder/dockerfile/evaluator.go @@ -34,7 +34,8 @@ import ( func dispatch(d dispatchRequest, cmd instructions.Command) error { if c, ok := cmd.(instructions.PlatformSpecific); ok { - err := c.CheckPlatform(d.builder.options.Platform.OS) + optionsOS := system.ParsePlatform(d.builder.options.Platform).OS + err := c.CheckPlatform(optionsOS) if err != nil { return validationError{err} } diff --git a/builder/dockerfile/imagecontext.go b/builder/dockerfile/imagecontext.go index ce83fe6230..79206dad4d 100644 --- a/builder/dockerfile/imagecontext.go +++ b/builder/dockerfile/imagecontext.go @@ -5,6 +5,7 @@ import ( "github.com/docker/docker/builder" "github.com/docker/docker/builder/remotecontext" dockerimage "github.com/docker/docker/image" + "github.com/docker/docker/pkg/system" "github.com/pkg/errors" "github.com/sirupsen/logrus" "golang.org/x/net/context" @@ -30,11 +31,12 @@ func newImageSources(ctx context.Context, options builderOptions) *imageSources pullOption = backend.PullOptionPreferLocal } } + optionsPlatform := system.ParsePlatform(options.Options.Platform) return options.Backend.GetImageAndReleasableLayer(ctx, idOrRef, backend.GetImageAndLayerOptions{ PullOption: pullOption, AuthConfig: options.Options.AuthConfigs, Output: options.ProgressWriter.Output, - OS: options.Options.Platform.OS, + OS: optionsPlatform.OS, }) } diff --git a/builder/dockerfile/internals.go b/builder/dockerfile/internals.go index ccdd68b276..0e08ec25f0 100644 --- a/builder/dockerfile/internals.go +++ b/builder/dockerfile/internals.go @@ -83,7 +83,8 @@ 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, b.options.Platform.OS)) + optionsPlatform := system.ParsePlatform(b.options.Platform) + runConfigWithCommentCmd := copyRunConfig(dispatchState.runConfig, withCmdComment(comment, optionsPlatform.OS)) hit, err := b.probeCache(dispatchState, runConfigWithCommentCmd) if err != nil || hit { return err @@ -122,7 +123,8 @@ 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(b.options.Platform.OS) + optionsPlatform := system.ParsePlatform(b.options.Platform) + newLayer, err := imageMount.Layer().Commit(optionsPlatform.OS) if err != nil { return err } @@ -170,9 +172,10 @@ func (b *Builder) performCopy(state *dispatchState, inst copyInstruction) error commentStr := fmt.Sprintf("%s %s%s in %s ", inst.cmdName, chownComment, srcHash, inst.dest) // TODO: should this have been using origPaths instead of srcHash in the comment? + optionsPlatform := system.ParsePlatform(b.options.Platform) runConfigWithCommentCmd := copyRunConfig( state.runConfig, - withCmdCommentString(commentStr, b.options.Platform.OS)) + withCmdCommentString(commentStr, optionsPlatform.OS)) hit, err := b.probeCache(state, runConfigWithCommentCmd) if err != nil || hit { return err @@ -183,7 +186,7 @@ func (b *Builder) performCopy(state *dispatchState, inst copyInstruction) error return errors.Wrapf(err, "failed to get destination image %q", state.imageID) } - destInfo, err := createDestInfo(state.runConfig.WorkingDir, inst, imageMount, b.options.Platform.OS) + destInfo, err := createDestInfo(state.runConfig.WorkingDir, inst, imageMount, b.options.Platform) if err != nil { return err } @@ -463,13 +466,15 @@ 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, b.options.Platform.OS) + optionsPlatform := system.ParsePlatform(b.options.Platform) + container, err := b.containerManager.Create(runConfig, hostConfig, optionsPlatform.OS) return container.ID, err } func (b *Builder) create(runConfig *container.Config) (string, error) { hostConfig := hostConfigFromOptions(b.options) - container, err := b.containerManager.Create(runConfig, hostConfig, b.options.Platform.OS) + optionsPlatform := system.ParsePlatform(b.options.Platform) + container, err := b.containerManager.Create(runConfig, hostConfig, optionsPlatform.OS) if err != nil { return "", err } diff --git a/client/image_build.go b/client/image_build.go index 9e12dcbbe0..cd0f54d138 100644 --- a/client/image_build.go +++ b/client/image_build.go @@ -7,12 +7,12 @@ import ( "net/http" "net/url" "strconv" + "strings" "golang.org/x/net/context" "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/container" - "github.com/docker/docker/pkg/system" ) // ImageBuild sends request to the daemon to build images. @@ -31,18 +31,11 @@ func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, optio } headers.Add("X-Registry-Config", base64.URLEncoding.EncodeToString(buf)) - // TODO @jhowardmsft: system.IsPlatformEmpty is a temporary function. We need to move - // (in the reasonably short future) to a package which supports all the platform - // validation such as is proposed in https://github.com/containerd/containerd/pull/1403 - if !system.IsPlatformEmpty(options.Platform) { + if options.Platform != "" { if err := cli.NewVersionError("1.32", "platform"); err != nil { return types.ImageBuildResponse{}, err } - platformJSON, err := json.Marshal(options.Platform) - if err != nil { - return types.ImageBuildResponse{}, err - } - headers.Add("X-Requested-Platform", string(platformJSON[:])) + query.Set("platform", options.Platform) } headers.Set("Content-Type", "application/x-tar") @@ -138,5 +131,8 @@ func (cli *Client) imageBuildOptionsToQuery(options types.ImageBuildOptions) (ur if options.SessionID != "" { query.Set("session", options.SessionID) } + if options.Platform != "" { + query.Set("platform", strings.ToLower(options.Platform)) + } return query, nil } diff --git a/client/image_create.go b/client/image_create.go index 6bc0984df4..fb5447b9b6 100644 --- a/client/image_create.go +++ b/client/image_create.go @@ -1,16 +1,14 @@ package client import ( - "encoding/json" "io" "net/url" + "strings" "golang.org/x/net/context" "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/system" - specs "github.com/opencontainers/image-spec/specs-go/v1" ) // ImageCreate creates a new image based in the parent options. @@ -24,25 +22,17 @@ func (cli *Client) ImageCreate(ctx context.Context, parentReference string, opti query := url.Values{} query.Set("fromImage", reference.FamiliarName(ref)) query.Set("tag", getAPITagFromNamedRef(ref)) - resp, err := cli.tryImageCreate(ctx, query, options.RegistryAuth, options.Platform) + if options.Platform != "" { + query.Set("platform", strings.ToLower(options.Platform)) + } + resp, err := cli.tryImageCreate(ctx, query, options.RegistryAuth) if err != nil { return nil, err } return resp.body, nil } -func (cli *Client) tryImageCreate(ctx context.Context, query url.Values, registryAuth string, platform specs.Platform) (serverResponse, error) { +func (cli *Client) tryImageCreate(ctx context.Context, query url.Values, registryAuth string) (serverResponse, error) { headers := map[string][]string{"X-Registry-Auth": {registryAuth}} - - // TODO @jhowardmsft: system.IsPlatformEmpty is a temporary function. We need to move - // (in the reasonably short future) to a package which supports all the platform - // validation such as is proposed in https://github.com/containerd/containerd/pull/1403 - if !system.IsPlatformEmpty(platform) { - platformJSON, err := json.Marshal(platform) - if err != nil { - return serverResponse{}, err - } - headers["X-Requested-Platform"] = []string{string(platformJSON[:])} - } return cli.post(ctx, "/images/create", query, nil, headers) } diff --git a/client/image_import.go b/client/image_import.go index d7dedd8232..ab55ddbacb 100644 --- a/client/image_import.go +++ b/client/image_import.go @@ -3,6 +3,7 @@ package client import ( "io" "net/url" + "strings" "golang.org/x/net/context" @@ -25,6 +26,9 @@ func (cli *Client) ImageImport(ctx context.Context, source types.ImageImportSour query.Set("repo", ref) query.Set("tag", options.Tag) query.Set("message", options.Message) + if options.Platform != "" { + query.Set("platform", strings.ToLower(options.Platform)) + } for _, change := range options.Changes { query.Add("changes", change) } diff --git a/client/image_pull.go b/client/image_pull.go index 5e14630834..92942d2e5b 100644 --- a/client/image_pull.go +++ b/client/image_pull.go @@ -4,12 +4,12 @@ import ( "io" "net/http" "net/url" + "strings" "golang.org/x/net/context" "github.com/docker/distribution/reference" "github.com/docker/docker/api/types" - "github.com/docker/docker/pkg/system" ) // ImagePull requests the docker host to pull an image from a remote registry. @@ -31,30 +31,17 @@ func (cli *Client) ImagePull(ctx context.Context, refStr string, options types.I if !options.All { query.Set("tag", getAPITagFromNamedRef(ref)) } - - // TODO 1: Extend to include "and the platform is supported by the daemon". - // This is dependent on https://github.com/moby/moby/pull/34628 though, - // and the daemon returning the set of platforms it supports via the _ping - // API endpoint. - // - // TODO 2: system.IsPlatformEmpty is a temporary function. We need to move - // (in the reasonably short future) to a package which supports all the platform - // validation such as is proposed in https://github.com/containerd/containerd/pull/1403 - // - // @jhowardmsft. - if !system.IsPlatformEmpty(options.Platform) { - if err := cli.NewVersionError("1.32", "platform"); err != nil { - return nil, err - } + if options.Platform != "" { + query.Set("platform", strings.ToLower(options.Platform)) } - resp, err := cli.tryImageCreate(ctx, query, options.RegistryAuth, options.Platform) + resp, err := cli.tryImageCreate(ctx, query, options.RegistryAuth) if resp.statusCode == http.StatusUnauthorized && options.PrivilegeFunc != nil { newAuthHeader, privilegeErr := options.PrivilegeFunc() if privilegeErr != nil { return nil, privilegeErr } - resp, err = cli.tryImageCreate(ctx, query, newAuthHeader, options.Platform) + resp, err = cli.tryImageCreate(ctx, query, newAuthHeader) } if err != nil { return nil, err diff --git a/container/container_unix.go b/container/container_unix.go index 6bb253fae3..796c48d984 100644 --- a/container/container_unix.go +++ b/container/container_unix.go @@ -66,7 +66,7 @@ func (container *Container) BuildHostnameFile() error { func (container *Container) NetworkMounts() []Mount { var mounts []Mount shared := container.HostConfig.NetworkMode.IsContainer() - parser := volume.NewParser(container.Platform) + parser := volume.NewParser(container.OS) if container.ResolvConfPath != "" { if _, err := os.Stat(container.ResolvConfPath); err != nil { logrus.Warnf("ResolvConfPath set to %q, but can't stat this filename (err = %v); skipping", container.ResolvConfPath, err) @@ -195,7 +195,7 @@ func (container *Container) UnmountIpcMount(unmount func(pth string) error) erro // IpcMounts returns the list of IPC mounts func (container *Container) IpcMounts() []Mount { var mounts []Mount - parser := volume.NewParser(container.Platform) + parser := volume.NewParser(container.OS) if container.HasMountFor("/dev/shm") { return mounts @@ -429,7 +429,7 @@ func copyOwnership(source, destination string) error { // TmpfsMounts returns the list of tmpfs mounts func (container *Container) TmpfsMounts() ([]Mount, error) { - parser := volume.NewParser(container.Platform) + parser := volume.NewParser(container.OS) var mounts []Mount for dest, data := range container.HostConfig.Tmpfs { mounts = append(mounts, Mount{ diff --git a/daemon/archive_unix.go b/daemon/archive_unix.go index e2d4c308d2..ae12cae704 100644 --- a/daemon/archive_unix.go +++ b/daemon/archive_unix.go @@ -12,7 +12,7 @@ import ( // cannot be configured with a read-only rootfs. func checkIfPathIsInAVolume(container *container.Container, absPath string) (bool, error) { var toVolume bool - parser := volume.NewParser(container.Platform) + parser := volume.NewParser(container.OS) for _, mnt := range container.MountPoints { if toVolume = parser.HasResource(mnt, absPath); toVolume { if mnt.RW { diff --git a/daemon/inspect.go b/daemon/inspect.go index 8d0529a70a..22551a702c 100644 --- a/daemon/inspect.go +++ b/daemon/inspect.go @@ -13,6 +13,7 @@ import ( "github.com/docker/docker/daemon/network" volumestore "github.com/docker/docker/volume/store" "github.com/docker/go-connections/nat" + specs "github.com/opencontainers/image-spec/specs-go/v1" ) // ContainerInspect returns low-level information about a @@ -171,7 +172,7 @@ func (daemon *Daemon) getInspectData(container *container.Container) (*types.Con Name: container.Name, RestartCount: container.RestartCount, Driver: container.Driver, - OS: container.OS, + Platform: specs.Platform{OS: container.OS}, MountLabel: container.MountLabel, ProcessLabel: container.ProcessLabel, ExecIDs: container.GetExecIDs(), diff --git a/distribution/pull_v1.go b/distribution/pull_v1.go index c53d30c407..34d892c3d8 100644 --- a/distribution/pull_v1.go +++ b/distribution/pull_v1.go @@ -12,6 +12,7 @@ import ( "time" "github.com/docker/distribution/reference" + "github.com/docker/distribution/registry/client/auth" "github.com/docker/distribution/registry/client/transport" "github.com/docker/docker/distribution/metadata" "github.com/docker/docker/distribution/xfer" @@ -68,7 +69,9 @@ func (p *v1Puller) Pull(ctx context.Context, ref reference.Named, platform strin return nil } -func (p *v1Puller) pullRepository(ctx context.Context, ref reference.Named) error { +// Note use auth.Scope rather than reference.Named due to this warning causing Jenkins CI to fail: +// warning: ref can be github.com/docker/docker/vendor/github.com/docker/distribution/registry/client/auth.Scope (interfacer) +func (p *v1Puller) pullRepository(ctx context.Context, ref auth.Scope) error { progress.Message(p.config.ProgressOutput, "", "Pulling repository "+p.repoInfo.Name.Name()) tagged, isTagged := ref.(reference.NamedTagged) diff --git a/distribution/pull_v2.go b/distribution/pull_v2.go index 76fc47d58c..c8d784ca63 100644 --- a/distribution/pull_v2.go +++ b/distribution/pull_v2.go @@ -510,7 +510,7 @@ func (p *v2Puller) pullSchema1(ctx context.Context, ref reference.Reference, unv // Early bath if the requested OS doesn't match that of the configuration. // This avoids doing the download, only to potentially fail later. - if !strings.EqualFold(string(configOS), requestedOS) { + if !strings.EqualFold(configOS, requestedOS) { return "", "", fmt.Errorf("cannot download image with operating system %q when requesting %q", configOS, requestedOS) } @@ -651,7 +651,7 @@ func (p *v2Puller) pullSchema2(ctx context.Context, ref reference.Named, mfst *s } if configJSON == nil { - configJSON, configRootFS, configOS, err = receiveConfig(p.config.ImageStore, configChan, configErrChan) + configJSON, configRootFS, _, err = receiveConfig(p.config.ImageStore, configChan, configErrChan) if err == nil && configRootFS == nil { err = errRootFSInvalid } @@ -723,7 +723,7 @@ func (p *v2Puller) pullManifestList(ctx context.Context, ref reference.Named, mf logrus.Debugf("%s resolved to a manifestList object with %d entries; looking for a %s/%s match", ref, len(mfstList.Manifests), os, runtime.GOARCH) - manifestMatches := filterManifests(mfstList.Manifests) + manifestMatches := filterManifests(mfstList.Manifests, os) if len(manifestMatches) == 0 { errMsg := fmt.Sprintf("no matching manifest for %s/%s in the manifest list entries", os, runtime.GOARCH) diff --git a/distribution/pull_v2_unix.go b/distribution/pull_v2_unix.go index 60d8605fb8..666ccc89cc 100644 --- a/distribution/pull_v2_unix.go +++ b/distribution/pull_v2_unix.go @@ -16,13 +16,13 @@ func (ld *v2LayerDescriptor) open(ctx context.Context) (distribution.ReadSeekClo return blobs.Open(ctx, ld.digest) } -func filterManifests(manifests []manifestlist.ManifestDescriptor) []manifestlist.ManifestDescriptor { +func filterManifests(manifests []manifestlist.ManifestDescriptor, os string) []manifestlist.ManifestDescriptor { var matches []manifestlist.ManifestDescriptor for _, manifestDescriptor := range manifests { - if manifestDescriptor.Platform.Architecture == runtime.GOARCH && manifestDescriptor.Platform.OS == runtime.GOOS { + if manifestDescriptor.Platform.Architecture == runtime.GOARCH && manifestDescriptor.Platform.OS == os { matches = append(matches, manifestDescriptor) - logrus.Debugf("found match for %s/%s with media type %s, digest %s", runtime.GOOS, runtime.GOARCH, manifestDescriptor.MediaType, manifestDescriptor.Digest.String()) + logrus.Debugf("found match for %s/%s with media type %s, digest %s", os, runtime.GOARCH, manifestDescriptor.MediaType, manifestDescriptor.Digest.String()) } } return matches diff --git a/distribution/pull_v2_windows.go b/distribution/pull_v2_windows.go index 5721761967..ffa1faddaa 100644 --- a/distribution/pull_v2_windows.go +++ b/distribution/pull_v2_windows.go @@ -62,29 +62,28 @@ func (ld *v2LayerDescriptor) open(ctx context.Context) (distribution.ReadSeekClo return rsc, err } -func filterManifests(manifests []manifestlist.ManifestDescriptor) []manifestlist.ManifestDescriptor { - version := system.GetOSVersion() - - // TODO @jhowardmsft LCOW Support: Need to remove the hard coding in LCOW mode. - lookingForOS := runtime.GOOS - osVersion := fmt.Sprintf("%d.%d.%d", version.MajorVersion, version.MinorVersion, version.Build) - if system.LCOWSupported() { - lookingForOS = "linux" - osVersion = "" +func filterManifests(manifests []manifestlist.ManifestDescriptor, os string) []manifestlist.ManifestDescriptor { + osVersion := "" + if os == "windows" { + version := system.GetOSVersion() + osVersion = fmt.Sprintf("%d.%d.%d", version.MajorVersion, version.MinorVersion, version.Build) + logrus.Debugf("will only match entries with version %s", osVersion) } var matches []manifestlist.ManifestDescriptor for _, manifestDescriptor := range manifests { - if manifestDescriptor.Platform.Architecture == runtime.GOARCH && manifestDescriptor.Platform.OS == lookingForOS { - if lookingForOS == "windows" && !versionMatch(manifestDescriptor.Platform.OSVersion, osVersion) { + if manifestDescriptor.Platform.Architecture == runtime.GOARCH && manifestDescriptor.Platform.OS == os { + if os == "windows" && !versionMatch(manifestDescriptor.Platform.OSVersion, osVersion) { + logrus.Debugf("skipping %s", manifestDescriptor.Platform.OSVersion) continue } matches = append(matches, manifestDescriptor) - - logrus.Debugf("found match for %s/%s with media type %s, digest %s", lookingForOS, runtime.GOARCH, manifestDescriptor.MediaType, manifestDescriptor.Digest.String()) + logrus.Debugf("found match for %s/%s with media type %s, digest %s", os, runtime.GOARCH, manifestDescriptor.MediaType, manifestDescriptor.Digest.String()) } } - sort.Stable(manifestsByVersion(matches)) + if os == "windows" { + sort.Stable(manifestsByVersion(matches)) + } return matches } diff --git a/distribution/registry_unit_test.go b/distribution/registry_unit_test.go index 4918534adf..db1814958e 100644 --- a/distribution/registry_unit_test.go +++ b/distribution/registry_unit_test.go @@ -4,6 +4,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "runtime" "strings" "testing" diff --git a/image/image_test.go b/image/image_test.go index 899666a60e..ba42db78bf 100644 --- a/image/image_test.go +++ b/image/image_test.go @@ -62,7 +62,7 @@ func TestImage(t *testing.T) { Domainname: "domain", User: "root", } - platform := runtime.GOOS + os := runtime.GOOS img := &Image{ V1Image: V1Image{ @@ -73,19 +73,19 @@ func TestImage(t *testing.T) { assert.Equal(t, cid, img.ImageID()) assert.Equal(t, cid, img.ID().String()) - assert.Equal(t, platform, img.Platform()) + assert.Equal(t, os, img.OperatingSystem()) assert.Equal(t, config, img.RunConfig()) } -func TestImagePlatformNotEmpty(t *testing.T) { - platform := "platform" +func TestImageOSNotEmpty(t *testing.T) { + os := "os" img := &Image{ V1Image: V1Image{ - OS: platform, + OS: os, }, OSVersion: "osversion", } - assert.Equal(t, platform, img.Platform()) + assert.Equal(t, os, img.OperatingSystem()) } func TestNewChildImageFromImageWithRootFS(t *testing.T) { diff --git a/pkg/system/lcow.go b/pkg/system/lcow.go index a1cf10f5e1..b88c11e316 100644 --- a/pkg/system/lcow.go +++ b/pkg/system/lcow.go @@ -8,21 +8,6 @@ import ( specs "github.com/opencontainers/image-spec/specs-go/v1" ) -// IsPlatformEmpty determines if an OCI image-spec platform structure is not populated. -// TODO This is a temporary function - can be replaced by parsing from -// https://github.com/containerd/containerd/pull/1403/files at a later date. -// @jhowardmsft -func IsPlatformEmpty(platform specs.Platform) bool { - if platform.Architecture == "" && - platform.OS == "" && - len(platform.OSFeatures) == 0 && - platform.OSVersion == "" && - platform.Variant == "" { - return true - } - return false -} - // ValidatePlatform determines if a platform structure is valid. // TODO This is a temporary function - can be replaced by parsing from // https://github.com/containerd/containerd/pull/1403/files at a later date. @@ -30,7 +15,9 @@ func IsPlatformEmpty(platform specs.Platform) bool { func ValidatePlatform(platform *specs.Platform) error { platform.Architecture = strings.ToLower(platform.Architecture) platform.OS = strings.ToLower(platform.OS) - if platform.Architecture != "" && platform.Architecture != runtime.GOARCH { + // Based on https://github.com/moby/moby/pull/34642#issuecomment-330375350, do + // not support anything except operating system. + if platform.Architecture != "" { return fmt.Errorf("invalid platform architecture %q", platform.Architecture) } if platform.OS != "" {