daemon: marshal legacy registry.ServiceConfig extra fields for compatability

This change moves the logic that is used to marshal the legacy extra fields for `registry.ServiceConfig` type to the daemon backend.

Signed-off-by: Austin Vazquez <austin.vazquez@docker.com>
This commit is contained in:
Austin Vazquez
2025-08-27 11:26:06 -05:00
parent b721c4f4a8
commit c9fdad2552
5 changed files with 81 additions and 67 deletions

View File

@@ -17,23 +17,6 @@ type ServiceConfig struct {
ExtraFields map[string]any `json:"-"`
}
// MarshalJSON implements a custom marshaler to include legacy fields
// in API responses.
func (sc *ServiceConfig) MarshalJSON() ([]byte, error) {
type tmp ServiceConfig
base, err := json.Marshal((*tmp)(sc))
if err != nil {
return nil, err
}
var merged map[string]any
_ = json.Unmarshal(base, &merged)
for k, v := range sc.ExtraFields {
merged[k] = v
}
return json.Marshal(merged)
}
// NetIPNet is the net.IPNet type, which can be marshalled and
// unmarshalled to JSON
type NetIPNet net.IPNet

View File

@@ -1,32 +0,0 @@
package registry
import (
"encoding/json"
"testing"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestServiceConfigMarshalLegacyFields(t *testing.T) {
t.Run("without legacy fields", func(t *testing.T) {
b, err := json.Marshal(&ServiceConfig{})
assert.NilError(t, err)
const expected = `{"IndexConfigs":null,"InsecureRegistryCIDRs":null,"Mirrors":null}`
assert.Check(t, is.Equal(string(b), expected), "Legacy nondistributable-artifacts fields should be omitted in output")
})
// Legacy fields should be returned when set to an empty slice. This is
// used for API versions < 1.49.
t.Run("with legacy fields", func(t *testing.T) {
b, err := json.Marshal(&ServiceConfig{
ExtraFields: map[string]any{
"AllowNondistributableArtifactsCIDRs": json.RawMessage(nil),
"AllowNondistributableArtifactsHostnames": json.RawMessage(nil),
},
})
assert.NilError(t, err)
const expected = `{"AllowNondistributableArtifactsCIDRs":null,"AllowNondistributableArtifactsHostnames":null,"IndexConfigs":null,"InsecureRegistryCIDRs":null,"Mirrors":null}`
assert.Check(t, is.Equal(string(b), expected))
})
}

View File

@@ -24,12 +24,21 @@ func (ir *infoResponse) MarshalJSON() ([]byte, error) {
if err != nil {
return nil, err
}
if len(ir.extraFields) == 0 {
if len(ir.extraFields) == 0 && (ir.Info == nil || ir.Info.RegistryConfig == nil || len(ir.Info.RegistryConfig.ExtraFields) == 0) {
return base, nil
}
var merged map[string]any
_ = json.Unmarshal(base, &merged)
// Merge top-level extraFields
maps.Copy(merged, ir.extraFields)
// Merge RegistryConfig.ExtraFields if present
if ir.Info != nil && ir.Info.RegistryConfig != nil && len(ir.Info.RegistryConfig.ExtraFields) > 0 {
if rc, ok := merged["RegistryConfig"].(map[string]any); ok {
maps.Copy(rc, ir.Info.RegistryConfig.ExtraFields)
}
}
return json.Marshal(merged)
}

View File

@@ -5,7 +5,10 @@ import (
"strings"
"testing"
"github.com/moby/moby/api/types/registry"
"github.com/moby/moby/api/types/system"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestLegacyFields(t *testing.T) {
@@ -31,3 +34,71 @@ func TestLegacyFields(t *testing.T) {
t.Errorf("legacy fields should contain %s: %s", expected, string(data))
}
}
// TestMarshalRegistryConfigLegacyFields verifies extra fields in the registry config
// field in the info response are serialized if they are not empty.
// This is used for backwards compatibility for API versions < 1.47.
func TestMarshalRegistryConfigLegacyFields(t *testing.T) {
expected := []string{"AllowNondistributableArtifactsCIDRs", "AllowNondistributableArtifactsHostnames"}
tests := []struct {
name string
info *infoResponse
assert func(t *testing.T, data []byte, err error)
}{
{
name: "without legacy fields",
info: &infoResponse{
Info: &system.Info{},
},
assert: func(t *testing.T, data []byte, err error) {
assert.NilError(t, err)
var resp map[string]any
err = json.Unmarshal(data, &resp)
assert.NilError(t, err)
rc, ok := resp["RegistryConfig"]
assert.Check(t, ok)
for _, v := range expected {
assert.Check(t, !is.Contains(rc, v)().Success())
}
},
},
{
name: "with legacy fields",
info: &infoResponse{
Info: &system.Info{
RegistryConfig: &registry.ServiceConfig{
ExtraFields: map[string]any{
"AllowNondistributableArtifactsCIDRs": json.RawMessage(nil),
"AllowNondistributableArtifactsHostnames": json.RawMessage(nil),
},
},
},
},
assert: func(t *testing.T, data []byte, err error) {
assert.NilError(t, err)
var resp map[string]any
err = json.Unmarshal(data, &resp)
assert.NilError(t, err)
rc, ok := resp["RegistryConfig"]
assert.Check(t, ok)
for _, v := range expected {
assert.Check(t, is.Contains(rc, v))
}
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
data, err := json.MarshalIndent(tc.info, "", " ")
tc.assert(t, data, err)
})
}
}

View File

@@ -17,23 +17,6 @@ type ServiceConfig struct {
ExtraFields map[string]any `json:"-"`
}
// MarshalJSON implements a custom marshaler to include legacy fields
// in API responses.
func (sc *ServiceConfig) MarshalJSON() ([]byte, error) {
type tmp ServiceConfig
base, err := json.Marshal((*tmp)(sc))
if err != nil {
return nil, err
}
var merged map[string]any
_ = json.Unmarshal(base, &merged)
for k, v := range sc.ExtraFields {
merged[k] = v
}
return json.Marshal(merged)
}
// NetIPNet is the net.IPNet type, which can be marshalled and
// unmarshalled to JSON
type NetIPNet net.IPNet