Browse code

Revert "Remove the rest of v1 manifest support"

This reverts commit 98fc09128b3f8221680bd22c2de5b893bd3d7ef8 in order to
keep registry v2 schema1 handling and libtrust-key-based engine ID.

Because registry v2 schema1 was not officially deprecated and
registries are still relying on it, this patch puts its logic back.

However, registry v1 relics are not added back since v1 logic has been
removed a while ago.

This also fixes an engine upgrade issue in a swarm cluster. It was relying
on the Engine ID to be the same upon upgrade, but the mentioned commit
modified the logic to use UUID and from a different file.

Since the libtrust key is always needed to support v2 schema1 pushes,
that the old engine ID is based on the libtrust key, and that the engine ID
needs to be conserved across upgrades, adding a UUID-based engine ID logic
seems to add more complexity than it solves the problems.

Hence reverting the engine ID changes as well.

Signed-off-by: Tibor Vass <tibor@docker.com>
(cherry picked from commit f695e98cb7cbb39eb7e5bd0a268ae1cd2e594fc1)
Signed-off-by: Tibor Vass <tibor@docker.com>

Tibor Vass authored on 2019/06/18 09:23:44
Showing 17 changed files
... ...
@@ -12,6 +12,8 @@ import (
12 12
 const (
13 13
 	// defaultShutdownTimeout is the default shutdown timeout for the daemon
14 14
 	defaultShutdownTimeout = 15
15
+	// defaultTrustKeyFile is the default filename for the trust key
16
+	defaultTrustKeyFile = "key.json"
15 17
 )
16 18
 
17 19
 // installCommonConfigFlags adds flags to the pflag.FlagSet to configure the daemon
... ...
@@ -81,6 +83,13 @@ func installCommonConfigFlags(conf *config.Config, flags *pflag.FlagSet) error {
81 81
 
82 82
 	flags.IntVar(&conf.NetworkControlPlaneMTU, "network-control-plane-mtu", config.DefaultNetworkMtu, "Network Control plane MTU")
83 83
 
84
+	// "--deprecated-key-path" is to allow configuration of the key used
85
+	// for the daemon ID and the deprecated image signing. It was never
86
+	// exposed as a command line option but is added here to allow
87
+	// overriding the default path in configuration.
88
+	flags.Var(opts.NewQuotedString(&conf.TrustKeyPath), "deprecated-key-path", "Path to key file for ID and image signing")
89
+	flags.MarkHidden("deprecated-key-path")
90
+
84 91
 	conf.MaxConcurrentDownloads = &maxConcurrentDownloads
85 92
 	conf.MaxConcurrentUploads = &maxConcurrentUploads
86 93
 	return nil
... ...
@@ -94,4 +103,10 @@ func installRegistryServiceFlags(options *registry.ServiceOptions, flags *pflag.
94 94
 	flags.Var(ana, "allow-nondistributable-artifacts", "Allow push of nondistributable artifacts to registry")
95 95
 	flags.Var(mirrors, "registry-mirror", "Preferred Docker registry mirror")
96 96
 	flags.Var(insecureRegistries, "insecure-registry", "Enable insecure registry communication")
97
+
98
+	if runtime.GOOS != "windows" {
99
+		// TODO: Remove this flag after 3 release cycles (18.03)
100
+		flags.BoolVar(&options.V2Only, "disable-legacy-registry", true, "Disable contacting legacy registries")
101
+		flags.MarkHidden("disable-legacy-registry")
102
+	}
97 103
 }
... ...
@@ -424,6 +424,14 @@ func loadDaemonCliConfig(opts *daemonOptions) (*config.Config, error) {
424 424
 		conf.CommonTLSOptions.KeyFile = opts.TLSOptions.KeyFile
425 425
 	}
426 426
 
427
+	if conf.TrustKeyPath == "" {
428
+		daemonConfDir, err := getDaemonConfDir(conf.Root)
429
+		if err != nil {
430
+			return nil, err
431
+		}
432
+		conf.TrustKeyPath = filepath.Join(daemonConfDir, defaultTrustKeyFile)
433
+	}
434
+
427 435
 	if flags.Changed("graph") && flags.Changed("data-root") {
428 436
 		return nil, errors.New(`cannot specify both "--graph" and "--data-root" option`)
429 437
 	}
... ...
@@ -446,6 +454,17 @@ func loadDaemonCliConfig(opts *daemonOptions) (*config.Config, error) {
446 446
 		return nil, err
447 447
 	}
448 448
 
449
+	if runtime.GOOS != "windows" {
450
+		if flags.Changed("disable-legacy-registry") {
451
+			// TODO: Remove this error after 3 release cycles (18.03)
452
+			return nil, errors.New("ERROR: The '--disable-legacy-registry' flag has been removed. Interacting with legacy (v1) registries is no longer supported")
453
+		}
454
+		if !conf.V2Only {
455
+			// TODO: Remove this error after 3 release cycles (18.03)
456
+			return nil, errors.New("ERROR: The 'disable-legacy-registry' configuration option has been removed. Interacting with legacy (v1) registries is no longer supported")
457
+		}
458
+	}
459
+
449 460
 	if flags.Changed("graph") {
450 461
 		logrus.Warnf(`The "-g / --graph" flag is deprecated. Please use "--data-root" instead`)
451 462
 	}
... ...
@@ -58,6 +58,10 @@ func setDefaultUmask() error {
58 58
 	return nil
59 59
 }
60 60
 
61
+func getDaemonConfDir(_ string) (string, error) {
62
+	return getDefaultDaemonConfigDir()
63
+}
64
+
61 65
 func (cli *DaemonCli) getPlatformContainerdDaemonOpts() ([]supervisor.DaemonOpt, error) {
62 66
 	opts := []supervisor.DaemonOpt{
63 67
 		supervisor.WithOOMScore(cli.Config.OOMScoreAdjust),
... ...
@@ -5,6 +5,7 @@ import (
5 5
 	"fmt"
6 6
 	"net"
7 7
 	"os"
8
+	"path/filepath"
8 9
 	"time"
9 10
 
10 11
 	"github.com/docker/docker/daemon/config"
... ...
@@ -23,6 +24,10 @@ func setDefaultUmask() error {
23 23
 	return nil
24 24
 }
25 25
 
26
+func getDaemonConfDir(root string) (string, error) {
27
+	return filepath.Join(root, `\config`), nil
28
+}
29
+
26 30
 // preNotifySystem sends a message to the host when the API is active, but before the daemon is
27 31
 func preNotifySystem() {
28 32
 	// start the service now to prevent timeouts waiting for daemon to start
... ...
@@ -8,6 +8,7 @@ import (
8 8
 	"io/ioutil"
9 9
 	"os"
10 10
 	"reflect"
11
+	"runtime"
11 12
 	"strings"
12 13
 	"sync"
13 14
 
... ...
@@ -134,6 +135,12 @@ type CommonConfig struct {
134 134
 	SocketGroup           string                    `json:"group,omitempty"`
135 135
 	CorsHeaders           string                    `json:"api-cors-header,omitempty"`
136 136
 
137
+	// TrustKeyPath is used to generate the daemon ID and for signing schema 1 manifests
138
+	// when pushing to a registry which does not support schema 2. This field is marked as
139
+	// deprecated because schema 1 manifests are deprecated in favor of schema 2 and the
140
+	// daemon ID will use a dedicated identifier not shared with exported signatures.
141
+	TrustKeyPath string `json:"deprecated-key-path,omitempty"`
142
+
137 143
 	// LiveRestoreEnabled determines whether we should keep containers
138 144
 	// alive upon daemon shutdown/start
139 145
 	LiveRestoreEnabled bool `json:"live-restore,omitempty"`
... ...
@@ -240,6 +247,9 @@ func New() *Config {
240 240
 	config.LogConfig.Config = make(map[string]string)
241 241
 	config.ClusterOpts = make(map[string]string)
242 242
 
243
+	if runtime.GOOS != "linux" {
244
+		config.V2Only = true
245
+	}
243 246
 	return &config
244 247
 }
245 248
 
... ...
@@ -960,7 +960,7 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S
960 960
 		return nil, err
961 961
 	}
962 962
 
963
-	uuid, err := loadOrCreateUUID(filepath.Join(config.Root, "engine_uuid"))
963
+	trustKey, err := loadOrCreateTrustKey(config.TrustKeyPath)
964 964
 	if err != nil {
965 965
 		return nil, err
966 966
 	}
... ...
@@ -1005,7 +1005,7 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S
1005 1005
 		return nil, errors.New("Devices cgroup isn't mounted")
1006 1006
 	}
1007 1007
 
1008
-	d.ID = uuid
1008
+	d.ID = trustKey.PublicKey().KeyID()
1009 1009
 	d.repository = daemonRepo
1010 1010
 	d.containers = container.NewMemoryStore()
1011 1011
 	if d.containersReplica, err = container.NewViewDB(); err != nil {
... ...
@@ -1036,6 +1036,7 @@ func NewDaemon(ctx context.Context, config *config.Config, pluginStore *plugin.S
1036 1036
 		MaxConcurrentUploads:      *config.MaxConcurrentUploads,
1037 1037
 		ReferenceStore:            rs,
1038 1038
 		RegistryService:           registryService,
1039
+		TrustKey:                  trustKey,
1039 1040
 	})
1040 1041
 
1041 1042
 	go d.execCommandGC()
... ...
@@ -54,6 +54,7 @@ func (i *ImageService) PushImage(ctx context.Context, image, tag string, metaHea
54 54
 		},
55 55
 		ConfigMediaType: schema2.MediaTypeImageConfig,
56 56
 		LayerStores:     distribution.NewLayerProvidersFromStores(i.layerStores),
57
+		TrustKey:        i.trustKey,
57 58
 		UploadManager:   i.uploadManager,
58 59
 	}
59 60
 
... ...
@@ -14,6 +14,7 @@ import (
14 14
 	"github.com/docker/docker/layer"
15 15
 	dockerreference "github.com/docker/docker/reference"
16 16
 	"github.com/docker/docker/registry"
17
+	"github.com/docker/libtrust"
17 18
 	"github.com/opencontainers/go-digest"
18 19
 	"github.com/pkg/errors"
19 20
 	"github.com/sirupsen/logrus"
... ...
@@ -39,6 +40,7 @@ type ImageServiceConfig struct {
39 39
 	MaxConcurrentUploads      int
40 40
 	ReferenceStore            dockerreference.Store
41 41
 	RegistryService           registry.Service
42
+	TrustKey                  libtrust.PrivateKey
42 43
 }
43 44
 
44 45
 // NewImageService returns a new ImageService from a configuration
... ...
@@ -54,6 +56,7 @@ func NewImageService(config ImageServiceConfig) *ImageService {
54 54
 		layerStores:               config.LayerStores,
55 55
 		referenceStore:            config.ReferenceStore,
56 56
 		registryService:           config.RegistryService,
57
+		trustKey:                  config.TrustKey,
57 58
 		uploadManager:             xfer.NewLayerUploadManager(config.MaxConcurrentUploads),
58 59
 	}
59 60
 }
... ...
@@ -69,6 +72,7 @@ type ImageService struct {
69 69
 	pruneRunning              int32
70 70
 	referenceStore            dockerreference.Store
71 71
 	registryService           registry.Service
72
+	trustKey                  libtrust.PrivateKey
72 73
 	uploadManager             *xfer.LayerUploadManager
73 74
 }
74 75
 
75 76
new file mode 100644
... ...
@@ -0,0 +1,57 @@
0
+package daemon // import "github.com/docker/docker/daemon"
1
+
2
+import (
3
+	"encoding/json"
4
+	"encoding/pem"
5
+	"fmt"
6
+	"os"
7
+	"path/filepath"
8
+
9
+	"github.com/docker/docker/pkg/ioutils"
10
+	"github.com/docker/docker/pkg/system"
11
+	"github.com/docker/libtrust"
12
+)
13
+
14
+// LoadOrCreateTrustKey attempts to load the libtrust key at the given path,
15
+// otherwise generates a new one
16
+// TODO: this should use more of libtrust.LoadOrCreateTrustKey which may need
17
+// a refactor or this function to be moved into libtrust
18
+func loadOrCreateTrustKey(trustKeyPath string) (libtrust.PrivateKey, error) {
19
+	err := system.MkdirAll(filepath.Dir(trustKeyPath), 0755, "")
20
+	if err != nil {
21
+		return nil, err
22
+	}
23
+	trustKey, err := libtrust.LoadKeyFile(trustKeyPath)
24
+	if err == libtrust.ErrKeyFileDoesNotExist {
25
+		trustKey, err = libtrust.GenerateECP256PrivateKey()
26
+		if err != nil {
27
+			return nil, fmt.Errorf("Error generating key: %s", err)
28
+		}
29
+		encodedKey, err := serializePrivateKey(trustKey, filepath.Ext(trustKeyPath))
30
+		if err != nil {
31
+			return nil, fmt.Errorf("Error serializing key: %s", err)
32
+		}
33
+		if err := ioutils.AtomicWriteFile(trustKeyPath, encodedKey, os.FileMode(0600)); err != nil {
34
+			return nil, fmt.Errorf("Error saving key file: %s", err)
35
+		}
36
+	} else if err != nil {
37
+		return nil, fmt.Errorf("Error loading key file %s: %s", trustKeyPath, err)
38
+	}
39
+	return trustKey, nil
40
+}
41
+
42
+func serializePrivateKey(key libtrust.PrivateKey, ext string) (encoded []byte, err error) {
43
+	if ext == ".json" || ext == ".jwk" {
44
+		encoded, err = json.Marshal(key)
45
+		if err != nil {
46
+			return nil, fmt.Errorf("unable to encode private key JWK: %s", err)
47
+		}
48
+	} else {
49
+		pemBlock, err := key.PEMBlock()
50
+		if err != nil {
51
+			return nil, fmt.Errorf("unable to encode private key PEM: %s", err)
52
+		}
53
+		encoded = pem.EncodeToMemory(pemBlock)
54
+	}
55
+	return
56
+}
0 57
new file mode 100644
... ...
@@ -0,0 +1,71 @@
0
+package daemon // import "github.com/docker/docker/daemon"
1
+
2
+import (
3
+	"io/ioutil"
4
+	"os"
5
+	"path/filepath"
6
+	"testing"
7
+
8
+	"gotest.tools/assert"
9
+	is "gotest.tools/assert/cmp"
10
+	"gotest.tools/fs"
11
+)
12
+
13
+// LoadOrCreateTrustKey
14
+func TestLoadOrCreateTrustKeyInvalidKeyFile(t *testing.T) {
15
+	tmpKeyFolderPath, err := ioutil.TempDir("", "api-trustkey-test")
16
+	assert.NilError(t, err)
17
+	defer os.RemoveAll(tmpKeyFolderPath)
18
+
19
+	tmpKeyFile, err := ioutil.TempFile(tmpKeyFolderPath, "keyfile")
20
+	assert.NilError(t, err)
21
+
22
+	_, err = loadOrCreateTrustKey(tmpKeyFile.Name())
23
+	assert.Check(t, is.ErrorContains(err, "Error loading key file"))
24
+}
25
+
26
+func TestLoadOrCreateTrustKeyCreateKeyWhenFileDoesNotExist(t *testing.T) {
27
+	tmpKeyFolderPath := fs.NewDir(t, "api-trustkey-test")
28
+	defer tmpKeyFolderPath.Remove()
29
+
30
+	// Without the need to create the folder hierarchy
31
+	tmpKeyFile := tmpKeyFolderPath.Join("keyfile")
32
+
33
+	key, err := loadOrCreateTrustKey(tmpKeyFile)
34
+	assert.NilError(t, err)
35
+	assert.Check(t, key != nil)
36
+
37
+	_, err = os.Stat(tmpKeyFile)
38
+	assert.NilError(t, err, "key file doesn't exist")
39
+}
40
+
41
+func TestLoadOrCreateTrustKeyCreateKeyWhenDirectoryDoesNotExist(t *testing.T) {
42
+	tmpKeyFolderPath := fs.NewDir(t, "api-trustkey-test")
43
+	defer tmpKeyFolderPath.Remove()
44
+	tmpKeyFile := tmpKeyFolderPath.Join("folder/hierarchy/keyfile")
45
+
46
+	key, err := loadOrCreateTrustKey(tmpKeyFile)
47
+	assert.NilError(t, err)
48
+	assert.Check(t, key != nil)
49
+
50
+	_, err = os.Stat(tmpKeyFile)
51
+	assert.NilError(t, err, "key file doesn't exist")
52
+}
53
+
54
+func TestLoadOrCreateTrustKeyCreateKeyNoPath(t *testing.T) {
55
+	defer os.Remove("keyfile")
56
+	key, err := loadOrCreateTrustKey("keyfile")
57
+	assert.NilError(t, err)
58
+	assert.Check(t, key != nil)
59
+
60
+	_, err = os.Stat("keyfile")
61
+	assert.NilError(t, err, "key file doesn't exist")
62
+}
63
+
64
+func TestLoadOrCreateTrustKeyLoadValidKey(t *testing.T) {
65
+	tmpKeyFile := filepath.Join("testdata", "keyfile")
66
+	key, err := loadOrCreateTrustKey(tmpKeyFile)
67
+	assert.NilError(t, err)
68
+	expected := "AWX2:I27X:WQFX:IOMK:CNAK:O7PW:VYNB:ZLKC:CVAE:YJP2:SI4A:XXAY"
69
+	assert.Check(t, is.Contains(key.String(), expected))
70
+}
0 71
deleted file mode 100644
... ...
@@ -1,35 +0,0 @@
1
-package daemon // import "github.com/docker/docker/daemon"
2
-
3
-import (
4
-	"fmt"
5
-	"io/ioutil"
6
-	"os"
7
-	"path/filepath"
8
-
9
-	"github.com/docker/docker/pkg/ioutils"
10
-	"github.com/google/uuid"
11
-)
12
-
13
-func loadOrCreateUUID(path string) (string, error) {
14
-	err := os.MkdirAll(filepath.Dir(path), 0755)
15
-	if err != nil {
16
-		return "", err
17
-	}
18
-	var id string
19
-	idb, err := ioutil.ReadFile(path)
20
-	if os.IsNotExist(err) {
21
-		id = uuid.New().String()
22
-		if err := ioutils.AtomicWriteFile(path, []byte(id), os.FileMode(0600)); err != nil {
23
-			return "", fmt.Errorf("Error saving uuid file: %s", err)
24
-		}
25
-	} else if err != nil {
26
-		return "", fmt.Errorf("Error loading uuid file %s: %s", path, err)
27
-	} else {
28
-		idp, err := uuid.Parse(string(idb))
29
-		if err != nil {
30
-			return "", fmt.Errorf("Error parsing uuid in file %s: %s", path, err)
31
-		}
32
-		id = idp.String()
33
-	}
34
-	return id, nil
35
-}
... ...
@@ -18,6 +18,7 @@ import (
18 18
 	"github.com/docker/docker/pkg/system"
19 19
 	refstore "github.com/docker/docker/reference"
20 20
 	"github.com/docker/docker/registry"
21
+	"github.com/docker/libtrust"
21 22
 	"github.com/opencontainers/go-digest"
22 23
 	specs "github.com/opencontainers/image-spec/specs-go/v1"
23 24
 )
... ...
@@ -72,6 +73,9 @@ type ImagePushConfig struct {
72 72
 	ConfigMediaType string
73 73
 	// LayerStores (indexed by operating system) manages layers.
74 74
 	LayerStores map[string]PushLayerProvider
75
+	// TrustKey is the private key for legacy signatures. This is typically
76
+	// an ephemeral key, since these signatures are no longer verified.
77
+	TrustKey libtrust.PrivateKey
75 78
 	// UploadManager dispatches uploads.
76 79
 	UploadManager *xfer.LayerUploadManager
77 80
 }
... ...
@@ -5,6 +5,7 @@ import (
5 5
 	"errors"
6 6
 	"fmt"
7 7
 	"io"
8
+	"runtime"
8 9
 	"sort"
9 10
 	"strings"
10 11
 	"sync"
... ...
@@ -180,8 +181,26 @@ func (p *v2Pusher) pushV2Tag(ctx context.Context, ref reference.NamedTagged, id
180 180
 
181 181
 	putOptions := []distribution.ManifestServiceOption{distribution.WithTag(ref.Tag())}
182 182
 	if _, err = manSvc.Put(ctx, manifest, putOptions...); err != nil {
183
-		logrus.Warnf("failed to upload schema2 manifest: %v", err)
184
-		return err
183
+		if runtime.GOOS == "windows" || p.config.TrustKey == nil || p.config.RequireSchema2 {
184
+			logrus.Warnf("failed to upload schema2 manifest: %v", err)
185
+			return err
186
+		}
187
+
188
+		logrus.Warnf("failed to upload schema2 manifest: %v - falling back to schema1", err)
189
+
190
+		manifestRef, err := reference.WithTag(p.repo.Named(), ref.Tag())
191
+		if err != nil {
192
+			return err
193
+		}
194
+		builder = schema1.NewConfigManifestBuilder(p.repo.Blobs(ctx), p.config.TrustKey, manifestRef, imgConfig)
195
+		manifest, err = manifestFromBuilder(ctx, builder, descriptors)
196
+		if err != nil {
197
+			return err
198
+		}
199
+
200
+		if _, err = manSvc.Put(ctx, manifest, putOptions...); err != nil {
201
+			return err
202
+		}
185 203
 	}
186 204
 
187 205
 	var canonicalManifest []byte
... ...
@@ -18,7 +18,6 @@ import (
18 18
 	"path"
19 19
 	"path/filepath"
20 20
 	"regexp"
21
-	"runtime"
22 21
 	"strconv"
23 22
 	"strings"
24 23
 	"sync"
... ...
@@ -36,6 +35,7 @@ import (
36 36
 	"github.com/docker/docker/pkg/mount"
37 37
 	"github.com/docker/go-units"
38 38
 	"github.com/docker/libnetwork/iptables"
39
+	"github.com/docker/libtrust"
39 40
 	"github.com/go-check/check"
40 41
 	"github.com/kr/pty"
41 42
 	"golang.org/x/sys/unix"
... ...
@@ -551,23 +551,20 @@ func (s *DockerDaemonSuite) TestDaemonAllocatesListeningPort(c *check.C) {
551 551
 	}
552 552
 }
553 553
 
554
-func (s *DockerDaemonSuite) TestDaemonUUIDGeneration(c *check.C) {
555
-	dir := "/var/lib/docker"
556
-	if runtime.GOOS == "windows" {
557
-		dir = filepath.Join(os.Getenv("programdata"), "docker")
558
-	}
559
-	file := filepath.Join(dir, "engine_uuid")
560
-	os.Remove(file)
554
+func (s *DockerDaemonSuite) TestDaemonKeyGeneration(c *check.C) {
555
+	// TODO: skip or update for Windows daemon
556
+	os.Remove("/etc/docker/key.json")
561 557
 	s.d.Start(c)
562 558
 	s.d.Stop(c)
563 559
 
564
-	fi, err := os.Stat(file)
560
+	k, err := libtrust.LoadKeyFile("/etc/docker/key.json")
565 561
 	if err != nil {
566
-		c.Fatalf("Error opening uuid file")
562
+		c.Fatalf("Error opening key file")
567 563
 	}
568
-	// Test for uuid length
569
-	if fi.Size() != 36 {
570
-		c.Fatalf("Bad UUID size %d", fi.Size())
564
+	kid := k.KeyID()
565
+	// Test Key ID is a valid fingerprint (e.g. QQXN:JY5W:TBXI:MK3X:GX6P:PD5D:F56N:NHCS:LVRZ:JA46:R24J:XEFF)
566
+	if len(kid) != 59 {
567
+		c.Fatalf("Bad key ID: %s", kid)
571 568
 	}
572 569
 }
573 570
 
... ...
@@ -92,7 +92,7 @@ func CreateInRegistry(ctx context.Context, repo string, auth *types.AuthConfig,
92 92
 		return nil, nil
93 93
 	}
94 94
 
95
-	regService, err := registry.NewService(registry.ServiceOptions{})
95
+	regService, err := registry.NewService(registry.ServiceOptions{V2Only: true})
96 96
 	if err != nil {
97 97
 		return err
98 98
 	}
... ...
@@ -19,11 +19,16 @@ type ServiceOptions struct {
19 19
 	AllowNondistributableArtifacts []string `json:"allow-nondistributable-artifacts,omitempty"`
20 20
 	Mirrors                        []string `json:"registry-mirrors,omitempty"`
21 21
 	InsecureRegistries             []string `json:"insecure-registries,omitempty"`
22
+
23
+	// V2Only controls access to legacy registries.  If it is set to true via the
24
+	// command line flag the daemon will not attempt to contact v1 legacy registries
25
+	V2Only bool `json:"disable-legacy-registry,omitempty"`
22 26
 }
23 27
 
24 28
 // serviceConfig holds daemon configuration for the registry service.
25 29
 type serviceConfig struct {
26 30
 	registrytypes.ServiceConfig
31
+	V2Only bool
27 32
 }
28 33
 
29 34
 var (
... ...
@@ -71,6 +76,7 @@ func newServiceConfig(options ServiceOptions) (*serviceConfig, error) {
71 71
 			// Hack: Bypass setting the mirrors to IndexConfigs since they are going away
72 72
 			// and Mirrors are only for the official registry anyways.
73 73
 		},
74
+		V2Only: options.V2Only,
74 75
 	}
75 76
 	if err := config.LoadAllowNondistributableArtifacts(options.AllowNondistributableArtifacts); err != nil {
76 77
 		return nil, err
... ...
@@ -309,5 +309,20 @@ func (s *DefaultService) LookupPushEndpoints(hostname string) (endpoints []APIEn
309 309
 }
310 310
 
311 311
 func (s *DefaultService) lookupEndpoints(hostname string) (endpoints []APIEndpoint, err error) {
312
-	return s.lookupV2Endpoints(hostname)
312
+	endpoints, err = s.lookupV2Endpoints(hostname)
313
+	if err != nil {
314
+		return nil, err
315
+	}
316
+
317
+	if s.config.V2Only {
318
+		return endpoints, nil
319
+	}
320
+
321
+	legacyEndpoints, err := s.lookupV1Endpoints(hostname)
322
+	if err != nil {
323
+		return nil, err
324
+	}
325
+	endpoints = append(endpoints, legacyEndpoints...)
326
+
327
+	return endpoints, nil
313 328
 }