Repository: containers/image
Pull request #834 (2020-03-02 14:07:56)

registries.conf.d: add stances for the registries.conf
When loading the registries.conf, allow for loading additional files
from `/etc/containers/registries.conf.d`.  The files are loaded in
alpha-numerical order and specified fields will overwrite the previous
config.

Signed-off-by: Valentin Rothberg <rothberg@redhat.com>

vrothberg: @mtrmac @baude @mrunalp PTAL
TomSweeneyRedHat: I'm not sure I'm seeing the need for this, but the code LGTM.

Patch:
diff --git a/docs/containers-registries.conf.d.5.md b/docs/containers-registries.conf.d.5.md
new file mode 100644
index 00000000..ca360c16
--- /dev/null
+++ b/docs/containers-registries.conf.d.5.md
@@ -0,0 +1,33 @@
+% CONTAINERS-REGISTRIES.CONF.D(5)
+% Valentin Rothberg
+% Mar 2020
+
+# NAME
+containers-registries.conf.d - directory for drop-in registries.conf files
+
+# DESCRIPTION
+CONTAINERS-REGISTRIES.CONF.D is a systemd-wide directory for drop-in
+configuration files in the `containers-registries.conf(5)` format.
+
+By default, the directory is located at `/etc/containers/registries.conf.d`.
+
+# CONFIGURATION PRECEDENCE
+
+Once the main configuration at `/etc/containers/registries.conf` is loaded, the
+files in `/etc/containers/registries.conf.d` are loaded in alpha-numerical order.
+Specified fields in a config will overwrite any previous setting.
+
+For instance, setting the `unqualified-search-registries` in
+`/etc/containers/registries.conf.d/myregistries.conf` will overwrite previous
+settings in `/etc/containers/registries.conf`.
+
+Note that all files must be specified in the same version of the
+`containers-registries.conf(5)` format.  The entire `[[registry]]` table will
+always be overridden if set.
+
+# SEE ALSO
+`containers-registries.conf(5)`
+
+# HISTORY
+
+Mar 2020, Originally compiled by Valentin Rothberg <rothberg@redhat.com>
diff --git a/go.mod b/go.mod
index aaf98370..0640efcb 100644
--- a/go.mod
+++ b/go.mod
@@ -39,10 +39,10 @@ require (
 	github.com/xeipuuv/gojsonpointer v0.0.0-20190809123943-df4f5c81cb3b // indirect
 	github.com/xeipuuv/gojsonschema v0.0.0-20190816131739-be0936907f66
 	go.etcd.io/bbolt v1.3.3 // indirect
-	golang.org/x/crypto v0.0.0-20191112222119-e1110fd1c708
+	golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6
 	golang.org/x/net v0.0.0-20190628185345-da137c7871d7
 	golang.org/x/sync v0.0.0-20190423024810-112230192c58
-	golang.org/x/sys v0.0.0-20191127021746-63cb32ae39b2
+	golang.org/x/sys v0.0.0-20200217220822-9197077df867
 	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
 	k8s.io/client-go v0.0.0-20170217214107-bcde30fb7eae
 )
diff --git a/pkg/sysregistriesv2/system_registries_v2.go b/pkg/sysregistriesv2/system_registries_v2.go
index 60d67dfd..18519914 100644
--- a/pkg/sysregistriesv2/system_registries_v2.go
+++ b/pkg/sysregistriesv2/system_registries_v2.go
@@ -2,7 +2,6 @@ package sysregistriesv2
 
 import (
 	"fmt"
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"regexp"
@@ -26,6 +25,16 @@ var systemRegistriesConfPath = builtinRegistriesConfPath
 // DO NOT change this, instead see systemRegistriesConfPath above.
 const builtinRegistriesConfPath = "/etc/containers/registries.conf"
 
+// systemRegistriesConfDirPath is the path to the system-wide registry
+// configuration directory and is used to add/subtract potential registries for
+// obtaining images.  You can override this at build time with
+// -ldflags '-X github.com/containers/image/sysregistries.systemRegistriesConfDirecotyPath=$your_path'
+var systemRegistriesConfDirPath = builtinRegistriesConfDirPath
+
+// builtinRegistriesConfDirPath is the path to the registry configuration directory.
+// DO NOT change this, instead see systemRegistriesConfDirectoryPath above.
+const builtinRegistriesConfDirPath = "/etc/containers/registries.conf.d"
+
 // Endpoint describes a remote location of a registry.
 type Endpoint struct {
 	// The endpoint's remote location.
@@ -360,16 +369,45 @@ func TryUpdatingCache(ctx *types.SystemContext) (*V2RegistriesConf, error) {
 	defer configMutex.Unlock()
 
 	// load the config
-	config, err := loadRegistryConf(configPath)
-	if err != nil {
+	config := &tomlConfig{}
+	if err := config.loadConfig(configPath); err != nil {
 		// Return an empty []Registry if we use the default config,
 		// which implies that the config path of the SystemContext
 		// isn't set.  Note: if ctx.SystemRegistriesConfPath points to
 		// the default config, we will still return an error.
 		if os.IsNotExist(err) && (ctx == nil || ctx.SystemRegistriesConfPath == "") {
-			return &V2RegistriesConf{Registries: []Registry{}}, nil
+			config = &tomlConfig{}
+			config.V2RegistriesConf = V2RegistriesConf{Registries: []Registry{}}
+		} else {
+			return nil, err
 		}
-		return nil, err
+	}
+
+	// Load the configs from the conf directory path.
+	confDir := systemRegistriesConfDirPath
+	if ctx != nil && ctx.SystemRegistriesConfDirPath != "" {
+		confDir = ctx.SystemRegistriesConfDirPath
+	}
+	err := filepath.Walk(confDir,
+		// WalkFunc to read additional
+		func(path string, info os.FileInfo, err error) error {
+			switch {
+			case info == nil:
+				// registries.conf.d doesn't exist
+				return nil
+			case info.IsDir():
+				// ignore directories
+				return nil
+			default:
+				// expect all files to be a config
+				return config.loadConfig(path)
+			}
+		},
+	)
+	if err != nil && !os.IsNotExist(err) {
+		// Ignore IsNotExist errors: most systems won't have a
+		// registries.conf.d directory.
+		return nil, errors.Wrapf(err, "error reading registries.conf.d")
 	}
 
 	v2Config := &config.V2RegistriesConf
@@ -470,16 +508,12 @@ func FindRegistry(ctx *types.SystemContext, ref string) (*Registry, error) {
 	return nil, nil
 }
 
-// Loads the registry configuration file from the filesystem and then unmarshals
-// it.  Returns the unmarshalled object.
-func loadRegistryConf(configPath string) (*tomlConfig, error) {
-	config := &tomlConfig{}
-
-	configBytes, err := ioutil.ReadFile(configPath)
-	if err != nil {
-		return nil, err
-	}
-
-	err = toml.Unmarshal(configBytes, &config)
-	return config, err
+// tomlConfig loads the unmarshals the configuration at the specified path.
+// Note that the tomlConfig's fields will be overridden if they are set in
+// specified path.  This behavior is necessary for registries.conf.d stances
+// behavior.
+func (t *tomlConfig) loadConfig(path string) error {
+	logrus.Debugf("Loading registries.conf %q", path)
+	_, err := toml.DecodeFile(path, t)
+	return err
 }
diff --git a/types/types.go b/types/types.go
index ba249ca2..36911f4d 100644
--- a/types/types.go
+++ b/types/types.go
@@ -497,6 +497,8 @@ type SystemContext struct {
 	RegistriesDirPath string
 	// Path to the system-wide registries configuration file
 	SystemRegistriesConfPath string
+	// Path to the system-wide registries configuration directory
+	SystemRegistriesConfDirPath string
 	// If not "", overrides the default path for the authentication file, but only new format files
 	AuthFilePath string
 	// if not "", overrides the default path for the authentication file, but with the legacy format;


Test patch:
diff --git a/pkg/sysregistriesv2/system_registries_v2_test.go b/pkg/sysregistriesv2/system_registries_v2_test.go
index afa306a3..c860d32e 100644
--- a/pkg/sysregistriesv2/system_registries_v2_test.go
+++ b/pkg/sysregistriesv2/system_registries_v2_test.go
@@ -463,3 +463,18 @@ func TestTryUpdatingCache(t *testing.T) {
 	assert.Nil(t, registries)
 	assert.Equal(t, 1, len(configCache))
 }
+
+func TestRegistriesConfDirectory(t *testing.T) {
+	ctx := &types.SystemContext{
+		SystemRegistriesConfPath:    "testdata/base-for-registries.d.conf",
+		SystemRegistriesConfDirPath: "testdata/registries.conf.d",
+	}
+	configCache = make(map[string]*V2RegistriesConf)
+	registries, err := TryUpdatingCache(ctx)
+	assert.Nil(t, err)
+	assert.NotNil(t, registries)
+
+	assert.Equal(t, registries.UnqualifiedSearchRegistries, []string{"example-overwrite.com"})
+	assert.Equal(t, len(registries.Registries), 1)
+	assert.Equal(t, registries.Registries[0].Location, "2.com")
+}
diff --git a/pkg/sysregistriesv2/testdata/base-for-registries.d.conf b/pkg/sysregistriesv2/testdata/base-for-registries.d.conf
new file mode 100644
index 00000000..f27ed071
--- /dev/null
+++ b/pkg/sysregistriesv2/testdata/base-for-registries.d.conf
@@ -0,0 +1,5 @@
+unqualified-search-registries = ["example.com"]
+
+[[registry]]
+location = "base.com"
+insecure = true
\ No newline at end of file
diff --git a/pkg/sysregistriesv2/testdata/registries.conf.d/config-1.conf b/pkg/sysregistriesv2/testdata/registries.conf.d/config-1.conf
new file mode 100644
index 00000000..e808cff7
--- /dev/null
+++ b/pkg/sysregistriesv2/testdata/registries.conf.d/config-1.conf
@@ -0,0 +1,4 @@
+unqualified-search-registries = ["example-overwrite.com"]
+
+[[registry]]
+location = "1.com"
\ No newline at end of file
diff --git a/pkg/sysregistriesv2/testdata/registries.conf.d/config-2.conf b/pkg/sysregistriesv2/testdata/registries.conf.d/config-2.conf
new file mode 100644
index 00000000..5f133433
--- /dev/null
+++ b/pkg/sysregistriesv2/testdata/registries.conf.d/config-2.conf
@@ -0,0 +1,2 @@
+[[registry]]
+location = "2.com"
\ No newline at end of file