diff --git a/client/client_test.go b/client/client_test.go index ace0060aa142..c846db9231af 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -10419,7 +10419,7 @@ func testFrontendVerifyPlatforms(t *testing.T, sb integration.Sandbox) { require.NoError(t, err) warnings = wc.wait() - require.Len(t, warnings, 0) + require.Len(t, warnings, 0, warningsListOutput(warnings)) frontend = func(ctx context.Context, c gateway.Client) (*gateway.Result, error) { res := gateway.NewResult() @@ -11016,3 +11016,17 @@ func testRunValidExitCodes(t *testing.T, sb integration.Sandbox) { require.Error(t, err) require.ErrorContains(t, err, "exit code: 0") } + +type warningsListOutput []*VertexWarning + +func (w warningsListOutput) String() string { + if len(w) == 0 { + return "" + } + var b strings.Builder + + for _, warn := range w { + _, _ = b.Write(warn.Short) + } + return b.String() +} diff --git a/client/llb/imagemetaresolver/resolver.go b/client/llb/imagemetaresolver/resolver.go index 0de55545b627..d5bfb1a469a9 100644 --- a/client/llb/imagemetaresolver/resolver.go +++ b/client/llb/imagemetaresolver/resolver.go @@ -107,7 +107,7 @@ func (imr *imageMetaResolver) ResolveImageConfig(ctx context.Context, ref string func (imr *imageMetaResolver) key(ref string, platform *ocispecs.Platform) string { if platform != nil { - ref += platforms.Format(*platform) + ref += platforms.FormatAll(*platform) } return ref } diff --git a/exporter/containerimage/annotations.go b/exporter/containerimage/annotations.go index 5973f628ad16..1bf10f6c15db 100644 --- a/exporter/containerimage/annotations.go +++ b/exporter/containerimage/annotations.go @@ -71,7 +71,7 @@ func (ag AnnotationsGroup) Platform(p *ocispecs.Platform) *Annotations { ps := []string{""} if p != nil { - ps = append(ps, platforms.Format(*p)) + ps = append(ps, platforms.FormatAll(*p)) } for _, a := range ag { diff --git a/exporter/containerimage/exptypes/annotations.go b/exporter/containerimage/exptypes/annotations.go index 37f1b205d15e..a9b74d6b9681 100644 --- a/exporter/containerimage/exptypes/annotations.go +++ b/exporter/containerimage/exptypes/annotations.go @@ -49,7 +49,7 @@ func (k AnnotationKey) PlatformString() string { if k.Platform == nil { return "" } - return platforms.Format(*k.Platform) + return platforms.FormatAll(*k.Platform) } func AnnotationIndexKey(key string) string { diff --git a/exporter/containerimage/exptypes/parse.go b/exporter/containerimage/exptypes/parse.go index bd6222338ef4..cd2256b566e9 100644 --- a/exporter/containerimage/exptypes/parse.go +++ b/exporter/containerimage/exptypes/parse.go @@ -53,7 +53,7 @@ func ParsePlatforms(meta map[string][]byte) (Platforms, error) { } } p = platforms.Normalize(p) - pk := platforms.Format(p) + pk := platforms.FormatAll(p) ps := Platforms{ Platforms: []Platform{{ID: pk, Platform: p}}, } diff --git a/exporter/verifier/platforms.go b/exporter/verifier/platforms.go index 5144b78e8e54..3bcc573c85dc 100644 --- a/exporter/verifier/platforms.go +++ b/exporter/verifier/platforms.go @@ -46,13 +46,14 @@ func CheckInvalidPlatforms[T comparable](ctx context.Context, res *result.Result }) } p = platforms.Normalize(p) - _, ok := reqMap[platforms.Format(p)] + formatted := platforms.FormatAll(p) + _, ok := reqMap[formatted] if ok { warnings = append(warnings, client.VertexWarning{ Short: []byte(fmt.Sprintf("Duplicate platform result requested %q", v)), }) } - reqMap[platforms.Format(p)] = struct{}{} + reqMap[formatted] = struct{}{} reqList = append(reqList, exptypes.Platform{Platform: p}) } @@ -62,10 +63,25 @@ func CheckInvalidPlatforms[T comparable](ctx context.Context, res *result.Result if len(reqMap) == 1 && len(ps.Platforms) == 1 { pp := platforms.Normalize(ps.Platforms[0].Platform) - if _, ok := reqMap[platforms.Format(pp)]; !ok { - return []client.VertexWarning{{ - Short: []byte(fmt.Sprintf("Requested platform %q does not match result platform %q", req.Platforms[0], platforms.Format(pp))), - }}, nil + if _, ok := reqMap[platforms.FormatAll(pp)]; !ok { + // The requested platform will often not have an OSVersion on it, but the + // resulting platform may have one. + // This should not be considered a mismatch, so check again after clearing + // the OSVersion from the returned platform. + reqP, err := platforms.Parse(req.Platforms[0]) + if err != nil { + return nil, err + } + reqP = platforms.Normalize(reqP) + if reqP.OSVersion == "" && reqP.OSVersion != pp.OSVersion { + pp.OSVersion = "" + } + + if _, ok := reqMap[platforms.FormatAll(pp)]; !ok { + return []client.VertexWarning{{ + Short: []byte(fmt.Sprintf("Requested platform %q does not match result platform %q", req.Platforms[0], platforms.FormatAll(pp))), + }}, nil + } } return nil, nil } @@ -81,7 +97,7 @@ func CheckInvalidPlatforms[T comparable](ctx context.Context, res *result.Result if !mismatch { for _, p := range ps.Platforms { pp := platforms.Normalize(p.Platform) - if _, ok := reqMap[platforms.Format(pp)]; !ok { + if _, ok := reqMap[platforms.FormatAll(pp)]; !ok { mismatch = true break } @@ -100,7 +116,7 @@ func CheckInvalidPlatforms[T comparable](ctx context.Context, res *result.Result func platformsString(ps []exptypes.Platform) string { var ss []string for _, p := range ps { - ss = append(ss, platforms.Format(platforms.Normalize(p.Platform))) + ss = append(ss, platforms.FormatAll(platforms.Normalize(p.Platform))) } sort.Strings(ss) return strings.Join(ss, ",") diff --git a/frontend/dockerfile/builder/build.go b/frontend/dockerfile/builder/build.go index 31ca7381c761..62b76bd5e22e 100644 --- a/frontend/dockerfile/builder/build.go +++ b/frontend/dockerfile/builder/build.go @@ -160,7 +160,7 @@ func Build(ctx context.Context, c client.Client) (_ *client.Result, err error) { if platform != nil { p = *platform } - scanTargets.Store(platforms.Format(platforms.Normalize(p)), scanTarget) + scanTargets.Store(platforms.FormatAll(platforms.Normalize(p)), scanTarget) return ref, img, baseImg, nil }) diff --git a/frontend/dockerfile/dockerfile2llb/convert.go b/frontend/dockerfile/dockerfile2llb/convert.go index 791f4d4a50ed..88e5943ebdcc 100644 --- a/frontend/dockerfile/dockerfile2llb/convert.go +++ b/frontend/dockerfile/dockerfile2llb/convert.go @@ -520,7 +520,7 @@ func toDispatchState(ctx context.Context, dt []byte, opt ConvertOpt) (*dispatchS if reachable { prefix := "[" if opt.MultiPlatformRequested && platform != nil { - prefix += platforms.Format(*platform) + " " + prefix += platforms.FormatAll(*platform) + " " } prefix += "internal]" mutRef, dgst, dt, err := metaResolver.ResolveImageConfig(ctx, d.stage.BaseName, sourceresolver.Opt{ @@ -2110,7 +2110,7 @@ func prefixCommand(ds *dispatchState, str string, prefixPlatform bool, platform } out := "[" if prefixPlatform && platform != nil { - out += platforms.Format(*platform) + formatTargetPlatform(*platform, platformFromEnv(env)) + " " + out += platforms.FormatAll(*platform) + formatTargetPlatform(*platform, platformFromEnv(env)) + " " } if ds.stageName != "" { out += ds.stageName + " " @@ -2144,7 +2144,7 @@ func formatTargetPlatform(base ocispecs.Platform, target *ocispecs.Platform) str return "->" + archVariant } if p.OS != base.OS { - return "->" + platforms.Format(p) + return "->" + platforms.FormatAll(p) } return "" } @@ -2491,8 +2491,8 @@ func wrapSuggestAny(err error, keys map[string]struct{}, options []string) error func validateBaseImagePlatform(name string, expected, actual ocispecs.Platform, location []parser.Range, lint *linter.Linter) { if expected.OS != actual.OS || expected.Architecture != actual.Architecture { - expectedStr := platforms.Format(platforms.Normalize(expected)) - actualStr := platforms.Format(platforms.Normalize(actual)) + expectedStr := platforms.FormatAll(platforms.Normalize(expected)) + actualStr := platforms.FormatAll(platforms.Normalize(actual)) msg := linter.RuleInvalidBaseImagePlatform.Format(name, expectedStr, actualStr) lint.Run(&linter.RuleInvalidBaseImagePlatform, location, msg) } diff --git a/frontend/dockerfile/dockerfile2llb/platform.go b/frontend/dockerfile/dockerfile2llb/platform.go index 5eb99d919a10..3a983fb86c62 100644 --- a/frontend/dockerfile/dockerfile2llb/platform.go +++ b/frontend/dockerfile/dockerfile2llb/platform.go @@ -45,10 +45,12 @@ func defaultArgs(po *platformOpt, overrides map[string]string, target string) *l s := [...][2]string{ {"BUILDPLATFORM", platforms.Format(bp)}, {"BUILDOS", bp.OS}, + {"BUILDOSVERSION", bp.OSVersion}, {"BUILDARCH", bp.Architecture}, {"BUILDVARIANT", bp.Variant}, - {"TARGETPLATFORM", platforms.Format(tp)}, + {"TARGETPLATFORM", platforms.FormatAll(tp)}, {"TARGETOS", tp.OS}, + {"TARGETOSVERSION", tp.OSVersion}, {"TARGETARCH", tp.Architecture}, {"TARGETVARIANT", tp.Variant}, {"TARGETSTAGE", target}, diff --git a/frontend/dockerfile/dockerfile_test.go b/frontend/dockerfile/dockerfile_test.go index f2493dad39aa..e2953aa05d74 100644 --- a/frontend/dockerfile/dockerfile_test.go +++ b/frontend/dockerfile/dockerfile_test.go @@ -215,6 +215,7 @@ var allTests = integration.TestFuncs( testStepNames, testPowershellInDefaultPathOnWindows, testOCILayoutMultiname, + testPlatformWithOSVersion, ) // Tests that depend on the `security.*` entitlements @@ -9425,6 +9426,158 @@ COPY Dockerfile /foo } } +func testPlatformWithOSVersion(t *testing.T, sb integration.Sandbox) { + // This test cannot be run on Windows currently due to `FROM scratch` and + // layer formatting not being supported on Windows. + integration.SkipOnPlatform(t, "windows") + + ctx := sb.Context() + + c, err := client.New(ctx, sb.Address()) + require.NoError(t, err) + defer c.Close() + + f := getFrontend(t, sb) + + // NOTE: currently "OS" *must* be set to "windows" for this to work. + // The platform matchers only do OSVersion comparisons when the OS is set to "windows". + p1 := ocispecs.Platform{ + OS: "windows", + OSVersion: "1.2.3", + Architecture: "bar", + } + p2 := ocispecs.Platform{ + OS: "windows", + OSVersion: "1.1.0", + Architecture: "bar", + } + + p1Str := platforms.FormatAll(p1) + p2Str := platforms.FormatAll(p2) + + registry, err := sb.NewRegistry() + if errors.Is(err, integration.ErrRequirements) { + t.Skip(err.Error()) + } + require.NoError(t, err) + target := registry + "/buildkit/testplatformwithosversion:latest" + + dockerfile := []byte(` +FROM ` + target + ` AS reg + +FROM scratch AS base +ARG TARGETOSVERSION +COPY <