Browse code

Merge pull request #51098 from thaJeztah/fix_image_inspect_legacy

image inspect: fix legacy fields for API < v1.52 response

Sebastiaan van Stijn authored on 2025/10/06 23:34:22
Showing 3 changed files
... ...
@@ -425,13 +425,17 @@ func (ir *imageRouter) getImagesByName(ctx context.Context, w http.ResponseWrite
425 425
 			"DockerVersion": imageInspect.DockerVersion, //nolint:staticcheck // ignore SA1019: field is deprecated, but still included in response when present.
426 426
 			"Author":        imageInspect.Author,
427 427
 		}))
428
+
429
+		// preserve fields in the image Config that have an "omitempty"
430
+		// in the OCI spec, but weren't omitted in API v1.51 and lower.
428 431
 		if versions.LessThan(version, "1.50") {
429
-			legacyOptions = append(legacyOptions, compat.WithExtraFields(legacyConfigFields["v1.49"]))
432
+			legacyOptions = append(legacyOptions, compat.WithExtraFields(map[string]any{
433
+				"Config": legacyConfigFields["v1.49"],
434
+			}))
430 435
 		} else {
431
-			// inspectResponse preserves fields in the response that have an
432
-			// "omitempty" in the OCI spec, but didn't omit such fields in
433
-			// legacy responses before API v1.50.
434
-			legacyOptions = append(legacyOptions, compat.WithExtraFields(legacyConfigFields["v1.50-v1.51"]))
436
+			legacyOptions = append(legacyOptions, compat.WithExtraFields(map[string]any{
437
+				"Config": legacyConfigFields["v1.50-v1.51"],
438
+			}))
435 439
 		}
436 440
 	}
437 441
 
... ...
@@ -1,12 +1,5 @@
1 1
 package image
2 2
 
3
-import (
4
-	"encoding/json"
5
-	"maps"
6
-
7
-	"github.com/moby/moby/api/types/image"
8
-)
9
-
10 3
 // legacyConfigFields defines legacy image-config fields to include in
11 4
 // API responses on older API versions.
12 5
 var legacyConfigFields = map[string]map[string]any{
... ...
@@ -31,9 +24,9 @@ var legacyConfigFields = map[string]map[string]any{
31 31
 		"Volumes":      nil,
32 32
 		"WorkingDir":   "",
33 33
 	},
34
-	// Legacy fields for current API versions (v1.50 and v1.52). These fields
35
-	// did not have an "omitempty" and were always included in the response,
36
-	// even if not set; see https://github.com/moby/moby/issues/50134
34
+	// Legacy fields for API v1.50 and v1.51. These fields did not have
35
+	// an "omitempty" and were always included in the response, even if
36
+	// not set; see https://github.com/moby/moby/issues/50134
37 37
 	"v1.50-v1.51": {
38 38
 		"Cmd":        nil,
39 39
 		"Entrypoint": nil,
... ...
@@ -45,41 +38,3 @@ var legacyConfigFields = map[string]map[string]any{
45 45
 		"WorkingDir": "",
46 46
 	},
47 47
 }
48
-
49
-// inspectCompatResponse is a wrapper around [image.InspectResponse] with a
50
-// custom marshal function for legacy [api/types/container.Config} fields
51
-// that have been removed, or did not have omitempty.
52
-type inspectCompatResponse struct {
53
-	*image.InspectResponse
54
-	legacyConfig map[string]any
55
-}
56
-
57
-// MarshalJSON implements a custom marshaler to include legacy fields
58
-// in API responses.
59
-func (ir *inspectCompatResponse) MarshalJSON() ([]byte, error) {
60
-	type tmp *image.InspectResponse
61
-	base, err := json.Marshal((tmp)(ir.InspectResponse))
62
-	if err != nil {
63
-		return nil, err
64
-	}
65
-	if len(ir.legacyConfig) == 0 {
66
-		return base, nil
67
-	}
68
-
69
-	type resp struct {
70
-		*image.InspectResponse
71
-		Config map[string]any
72
-	}
73
-
74
-	var merged resp
75
-	err = json.Unmarshal(base, &merged)
76
-	if err != nil {
77
-		return base, nil
78
-	}
79
-
80
-	// prevent mutating legacyConfigFields.
81
-	cfg := maps.Clone(ir.legacyConfig)
82
-	maps.Copy(cfg, merged.Config)
83
-	merged.Config = cfg
84
-	return json.Marshal(merged)
85
-}
... ...
@@ -6,6 +6,7 @@ import (
6 6
 
7 7
 	dockerspec "github.com/moby/docker-image-spec/specs-go/v1"
8 8
 	"github.com/moby/moby/api/types/image"
9
+	"github.com/moby/moby/v2/daemon/internal/compat"
9 10
 	ocispec "github.com/opencontainers/image-spec/specs-go/v1"
10 11
 	"gotest.tools/v3/assert"
11 12
 	is "gotest.tools/v3/assert/cmp"
... ...
@@ -59,10 +60,11 @@ func TestInspectResponse(t *testing.T) {
59 59
 					ImageConfig: *tc.cfg,
60 60
 				}
61 61
 			}
62
-			out, err := json.Marshal(&inspectCompatResponse{
63
-				InspectResponse: imgInspect,
64
-				legacyConfig:    tc.legacyConfig,
65
-			})
62
+			legacyConfigResponse := compat.Wrap(imgInspect, compat.WithExtraFields(map[string]any{
63
+				"Config": tc.legacyConfig,
64
+			}))
65
+
66
+			out, err := json.Marshal(&legacyConfigResponse)
66 67
 			assert.NilError(t, err)
67 68
 
68 69
 			var outMap struct{ Config json.RawMessage }