-
Notifications
You must be signed in to change notification settings - Fork 199
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a new concept to containers.conf called "modules". A "module" is a containers.conf file located at a specific directory. More than one modules can be loaded in the specified order, following existing override semantics. There are three directories to load modules from: - $CONFIG_HOME/containers/containers.conf.modules - /etc/containers/containers.conf.modules - /usr/share/containers/containers.conf.modules With CONFIG_HOME pointing to $HOME/.config or, if set, $XDG_CONFIG_HOME. Absolute paths will be loaded as is, relative paths will be resolved relative to the three directories above allowing for admin configs (/etc/) to override system configs (/usr/share/) and user configs ($CONFIG_HOME) to override admin configs. Also move some functions from config.go for locality. Signed-off-by: Valentin Rothberg <[email protected]>
- Loading branch information
Showing
20 changed files
with
425 additions
and
116 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
package config | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
|
||
"github.com/containers/storage/pkg/homedir" | ||
"github.com/containers/storage/pkg/unshare" | ||
"github.com/hashicorp/go-multierror" | ||
) | ||
|
||
// The subdirectory for looking up containers.conf modules. | ||
const moduleSubdir = "containers/containers.conf.modules" | ||
|
||
// Moving the base paths into variables allows for overriding them in units | ||
// tests. | ||
var ( | ||
moduleBaseEtc = "/etc/" | ||
moduleBaseUsr = "/usr/share" | ||
) | ||
|
||
// Find the specified modules in the options. Return an error if a specific | ||
// module cannot be located on the host. | ||
func (o *Options) modules() ([]string, error) { | ||
if len(o.Modules) == 0 { | ||
return nil, nil | ||
} | ||
|
||
dirs, err := moduleDirectories() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
configs := make([]string, 0, len(o.Modules)) | ||
for _, path := range o.Modules { | ||
resolved, err := resolveModule(path, dirs) | ||
if err != nil { | ||
return nil, fmt.Errorf("could not resolve module %q: %w", path, err) | ||
} | ||
configs = append(configs, resolved) | ||
} | ||
|
||
return configs, nil | ||
} | ||
|
||
// Return the directories to load modules from: | ||
// 1) XDG_CONFIG_HOME/HOME if rootless | ||
// 2) /etc/ | ||
// 3) /usr/share | ||
func moduleDirectories() ([]string, error) { | ||
modules := []string{ | ||
filepath.Join(moduleBaseEtc, moduleSubdir), | ||
filepath.Join(moduleBaseUsr, moduleSubdir), | ||
} | ||
|
||
if !unshare.IsRootless() { | ||
return modules, nil | ||
} | ||
|
||
// Prepend the user modules dir. | ||
configHome, err := homedir.GetConfigHome() | ||
if err != nil { | ||
return nil, err | ||
} | ||
return append([]string{filepath.Join(configHome, moduleSubdir)}, modules...), nil | ||
} | ||
|
||
// Resolve the specified path to a module. | ||
func resolveModule(path string, dirs []string) (string, error) { | ||
if filepath.IsAbs(path) { | ||
_, err := os.Stat(path) | ||
return path, err | ||
} | ||
|
||
// Collect all errors to avoid suppressing important errors (e.g., | ||
// permission errors). | ||
var multiErr error | ||
for _, d := range dirs { | ||
candidate := filepath.Join(d, path) | ||
_, err := os.Stat(candidate) | ||
if err == nil { | ||
return candidate, nil | ||
} | ||
multiErr = multierror.Append(multiErr, err) | ||
} | ||
return "", multiErr | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,169 @@ | ||
package config | ||
|
||
import ( | ||
"os" | ||
"path/filepath" | ||
|
||
"github.com/containers/storage/pkg/unshare" | ||
. "github.com/onsi/ginkgo/v2" | ||
"github.com/onsi/gomega" | ||
) | ||
|
||
const ( | ||
testBaseHome = "testdata/modules/home/.config" | ||
testBaseEtc = "testdata/modules/etc" | ||
testBaseUsr = "testdata/modules/usr/share" | ||
) | ||
|
||
func testSetModulePaths() (func(), error) { | ||
oldXDG := os.Getenv("XDG_CONFIG_HOME") | ||
oldEtc := moduleBaseEtc | ||
oldUsr := moduleBaseUsr | ||
|
||
wd, err := os.Getwd() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if err := os.Setenv("XDG_CONFIG_HOME", filepath.Join(wd, testBaseHome)); err != nil { | ||
return nil, err | ||
} | ||
|
||
moduleBaseEtc = filepath.Join(wd, testBaseEtc) | ||
moduleBaseUsr = filepath.Join(wd, testBaseUsr) | ||
|
||
return func() { | ||
os.Setenv("XDG_CONFIG_HOME", oldXDG) | ||
moduleBaseEtc = oldEtc | ||
moduleBaseUsr = oldUsr | ||
}, nil | ||
} | ||
|
||
var _ = Describe("Config Modules", func() { | ||
It("module directories", func() { | ||
dirs, err := moduleDirectories() | ||
gomega.Expect(err).To(gomega.BeNil()) | ||
gomega.Expect(dirs).NotTo(gomega.BeNil()) | ||
|
||
if unshare.IsRootless() { | ||
gomega.Expect(dirs).To(gomega.HaveLen(3)) | ||
} else { | ||
gomega.Expect(dirs).To(gomega.HaveLen(2)) | ||
} | ||
}) | ||
|
||
It("resolve modules", func() { | ||
// This test makes sure that the correct module is being | ||
// returned. | ||
cleanUp, err := testSetModulePaths() | ||
gomega.Expect(err).To(gomega.BeNil()) | ||
defer cleanUp() | ||
|
||
dirs, err := moduleDirectories() | ||
gomega.Expect(err).To(gomega.BeNil()) | ||
|
||
if unshare.IsRootless() { | ||
gomega.Expect(dirs).To(gomega.HaveLen(3)) | ||
gomega.Expect(dirs[0]).To(gomega.ContainSubstring(testBaseHome)) | ||
gomega.Expect(dirs[1]).To(gomega.ContainSubstring(testBaseEtc)) | ||
gomega.Expect(dirs[2]).To(gomega.ContainSubstring(testBaseUsr)) | ||
} else { | ||
gomega.Expect(dirs).To(gomega.HaveLen(2)) | ||
gomega.Expect(dirs[0]).To(gomega.ContainSubstring(testBaseEtc)) | ||
gomega.Expect(dirs[1]).To(gomega.ContainSubstring(testBaseUsr)) | ||
} | ||
|
||
for _, test := range []struct { | ||
input string | ||
expectedDir string | ||
mustFail bool | ||
rootless bool | ||
}{ | ||
// Rootless | ||
{"first.conf", testBaseHome, false, true}, | ||
{"second.conf", testBaseHome, false, true}, | ||
{"third.conf", testBaseHome, false, true}, | ||
{"sub/first.conf", testBaseHome, false, true}, | ||
|
||
// Root + Rootless | ||
{"fourth.conf", testBaseEtc, false, false}, | ||
{"sub/etc-only.conf", testBaseEtc, false, false}, | ||
{"fifth.conf", testBaseUsr, false, false}, | ||
{"sub/share-only.conf", testBaseUsr, false, false}, | ||
{"none.conf", "", true, false}, | ||
} { | ||
if test.rootless && !unshare.IsRootless() { | ||
continue | ||
} | ||
result, err := resolveModule(test.input, dirs) | ||
if test.mustFail { | ||
gomega.Expect(err).NotTo(gomega.BeNil()) | ||
continue | ||
} | ||
gomega.Expect(err).To(gomega.BeNil()) | ||
gomega.Expect(result).To(gomega.HaveSuffix(filepath.Join(test.expectedDir, moduleSubdir, test.input))) | ||
} | ||
}) | ||
|
||
It("new config with modules", func() { | ||
cleanUp, err := testSetModulePaths() | ||
gomega.Expect(err).To(gomega.BeNil()) | ||
defer cleanUp() | ||
|
||
options := &Options{Modules: []string{"none.conf"}} | ||
_, err = New(options) | ||
gomega.Expect(err).NotTo(gomega.BeNil()) // must error out | ||
|
||
options = &Options{} | ||
c, err := New(options) | ||
gomega.Expect(err).To(gomega.BeNil()) | ||
gomega.Expect(options.additionalConfigs).To(gomega.HaveLen(0)) // no module is getting loaded! | ||
gomega.Expect(c).NotTo(gomega.BeNil()) | ||
|
||
options = &Options{Modules: []string{"fourth.conf"}} | ||
c, err = New(options) | ||
gomega.Expect(err).To(gomega.BeNil()) | ||
gomega.Expect(options.additionalConfigs).To(gomega.HaveLen(1)) // 1 module is getting loaded! | ||
gomega.Expect(c.Containers.InitPath).To(gomega.Equal("etc four")) | ||
|
||
options = &Options{Modules: []string{"fourth.conf"}} | ||
c, err = New(options) | ||
gomega.Expect(err).To(gomega.BeNil()) | ||
gomega.Expect(options.additionalConfigs).To(gomega.HaveLen(1)) // 1 module is getting loaded! | ||
gomega.Expect(c.Containers.InitPath).To(gomega.Equal("etc four")) | ||
|
||
options = &Options{Modules: []string{"fourth.conf", "sub/share-only.conf"}} | ||
c, err = New(options) | ||
gomega.Expect(err).To(gomega.BeNil()) | ||
gomega.Expect(options.additionalConfigs).To(gomega.HaveLen(2)) // 2 modules are getting loaded! | ||
gomega.Expect(c.Containers.InitPath).To(gomega.Equal("etc four")) | ||
gomega.Expect(c.Containers.Env).To(gomega.Equal([]string{"usr share only"})) | ||
}) | ||
|
||
It("new config with modules and env variables", func() { | ||
cleanUp, err := testSetModulePaths() | ||
gomega.Expect(err).To(gomega.BeNil()) | ||
defer cleanUp() | ||
|
||
oldOverride := os.Getenv(containersConfOverrideEnv) | ||
defer func() { | ||
os.Setenv(containersConfOverrideEnv, oldOverride) | ||
}() | ||
|
||
err = os.Setenv(containersConfOverrideEnv, "testdata/modules/override.conf") | ||
gomega.Expect(err).To(gomega.BeNil()) | ||
|
||
// Also make sure that absolute paths are loaded as is. | ||
wd, err := os.Getwd() | ||
gomega.Expect(err).To(gomega.BeNil()) | ||
absConf := filepath.Join(wd, "testdata/modules/home/.config/containers/containers.conf.modules/second.conf") | ||
|
||
options := &Options{Modules: []string{"fourth.conf", "sub/share-only.conf", absConf}} | ||
c, err := New(options) | ||
gomega.Expect(err).To(gomega.BeNil()) | ||
gomega.Expect(options.additionalConfigs).To(gomega.HaveLen(4)) // 2 modules + abs path + override conf are getting loaded! | ||
gomega.Expect(c.Containers.InitPath).To(gomega.Equal("etc four")) | ||
gomega.Expect(c.Containers.Env).To(gomega.Equal([]string{"override conf always wins"})) | ||
gomega.Expect(c.Containers.Volumes).To(gomega.Equal([]string{"home second"})) | ||
}) | ||
}) |
Oops, something went wrong.