Support config imports #3289
Signed-off-by: Maksym Pavlenko <makpav@amazon.com>
This commit is contained in:
parent
6e2228df72
commit
a1e3779cad
@ -17,12 +17,15 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
"github.com/BurntSushi/toml"
|
||||||
|
"github.com/imdario/mergo"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/containerd/containerd/errdefs"
|
"github.com/containerd/containerd/errdefs"
|
||||||
"github.com/containerd/containerd/plugin"
|
"github.com/containerd/containerd/plugin"
|
||||||
"github.com/pkg/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config provides containerd configuration data for the server
|
// Config provides containerd configuration data for the server
|
||||||
@ -57,6 +60,8 @@ type Config struct {
|
|||||||
ProxyPlugins map[string]ProxyPlugin `toml:"proxy_plugins"`
|
ProxyPlugins map[string]ProxyPlugin `toml:"proxy_plugins"`
|
||||||
// Timeouts specified as a duration
|
// Timeouts specified as a duration
|
||||||
Timeouts map[string]string `toml:"timeouts"`
|
Timeouts map[string]string `toml:"timeouts"`
|
||||||
|
// Imports are additional file path list to config files that can overwrite main config file fields
|
||||||
|
Imports []string `toml:"imports"`
|
||||||
|
|
||||||
StreamProcessors []StreamProcessor `toml:"stream_processors"`
|
StreamProcessors []StreamProcessor `toml:"stream_processors"`
|
||||||
|
|
||||||
@ -205,16 +210,102 @@ func (c *Config) Decode(p *plugin.Registration) (interface{}, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfig loads the containerd server config from the provided path
|
// LoadConfig loads the containerd server config from the provided path
|
||||||
func LoadConfig(path string, v *Config) error {
|
func LoadConfig(path string, out *Config) error {
|
||||||
if v == nil {
|
if out == nil {
|
||||||
return errors.Wrapf(errdefs.ErrInvalidArgument, "argument v must not be nil")
|
return errors.Wrapf(errdefs.ErrInvalidArgument, "argument out must not be nil")
|
||||||
}
|
}
|
||||||
md, err := toml.DecodeFile(path, v)
|
|
||||||
|
var (
|
||||||
|
loaded = map[string]bool{}
|
||||||
|
pending = []string{path}
|
||||||
|
)
|
||||||
|
|
||||||
|
for len(pending) > 0 {
|
||||||
|
path, pending = pending[0], pending[1:]
|
||||||
|
|
||||||
|
// Check if a file at the given path already loaded to prevent circular imports
|
||||||
|
if _, ok := loaded[path]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
config, err := loadConfigFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := mergeConfig(out, config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
imports, err := resolveImports(path, config.Imports)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
loaded[path] = true
|
||||||
|
pending = append(pending, imports...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix up the list of config files loaded
|
||||||
|
out.Imports = []string{}
|
||||||
|
for path := range loaded {
|
||||||
|
out.Imports = append(out.Imports, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.ValidateV2()
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadConfigFile decodes a TOML file at the given path
|
||||||
|
func loadConfigFile(path string) (*Config, error) {
|
||||||
|
config := &Config{}
|
||||||
|
md, err := toml.DecodeFile(path, &config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
v.md = md
|
config.md = md
|
||||||
return v.ValidateV2()
|
return config, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveImports resolves import strings list to absolute paths list:
|
||||||
|
// - If path contains *, glob pattern matching applied
|
||||||
|
// - Non abs path is relative to parent config file directory
|
||||||
|
// - Abs paths returned as is
|
||||||
|
func resolveImports(parent string, imports []string) ([]string, error) {
|
||||||
|
var out []string
|
||||||
|
|
||||||
|
for _, path := range imports {
|
||||||
|
if strings.Contains(path, "*") {
|
||||||
|
matches, err := filepath.Glob(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, matches...)
|
||||||
|
} else {
|
||||||
|
path = filepath.Clean(path)
|
||||||
|
if !filepath.IsAbs(path) {
|
||||||
|
path = filepath.Join(filepath.Dir(parent), path)
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// mergeConfig merges Config structs with the following rules:
|
||||||
|
// 'to' 'from' 'result' overwrite?
|
||||||
|
// "" "value" "value" yes
|
||||||
|
// "value" "" "value" no
|
||||||
|
// 1 0 1 no
|
||||||
|
// 0 1 1 yes
|
||||||
|
// []{"1"} []{"2"} []{"2"} yes
|
||||||
|
// []{"1"} []{} []{"1"} no
|
||||||
|
func mergeConfig(to, from *Config) error {
|
||||||
|
return mergo.Merge(to, from, func(config *mergo.Config) {
|
||||||
|
config.Overwrite = true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// V1DisabledFilter matches based on ID
|
// V1DisabledFilter matches based on ID
|
||||||
|
163
services/server/config/config_test.go
Normal file
163
services/server/config/config_test.go
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
/*
|
||||||
|
Copyright The containerd Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"gotest.tools/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMergeConfigs(t *testing.T) {
|
||||||
|
a := &Config{
|
||||||
|
Version: 2,
|
||||||
|
Root: "old_root",
|
||||||
|
RequiredPlugins: []string{"old_plugin"},
|
||||||
|
DisabledPlugins: []string{"old_plugin"},
|
||||||
|
State: "old_state",
|
||||||
|
OOMScore: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
b := &Config{
|
||||||
|
Root: "new_root",
|
||||||
|
RequiredPlugins: []string{"new_plugin1", "new_plugin2"},
|
||||||
|
OOMScore: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := mergeConfig(a, b)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, a.Version, 2)
|
||||||
|
assert.Equal(t, a.Root, "new_root")
|
||||||
|
assert.Equal(t, a.State, "old_state")
|
||||||
|
assert.Equal(t, a.OOMScore, 2)
|
||||||
|
assert.DeepEqual(t, a.RequiredPlugins, []string{"new_plugin1", "new_plugin2"})
|
||||||
|
assert.DeepEqual(t, a.DisabledPlugins, []string{"old_plugin"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveImports(t *testing.T) {
|
||||||
|
tempDir, err := ioutil.TempDir("", "containerd_")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
for _, filename := range []string{"config_1.toml", "config_2.toml", "test.toml"} {
|
||||||
|
err = ioutil.WriteFile(filepath.Join(tempDir, filename), []byte(""), 0600)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
imports, err := resolveImports(filepath.Join(tempDir, "root.toml"), []string{
|
||||||
|
filepath.Join(tempDir, "config_*.toml"), // Glob
|
||||||
|
filepath.Join(tempDir, "./test.toml"), // Path clean up
|
||||||
|
"current.toml", // Resolve current working dir
|
||||||
|
})
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
assert.DeepEqual(t, imports, []string{
|
||||||
|
filepath.Join(tempDir, "config_1.toml"),
|
||||||
|
filepath.Join(tempDir, "config_2.toml"),
|
||||||
|
filepath.Join(tempDir, "test.toml"),
|
||||||
|
filepath.Join(tempDir, "current.toml"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadSingleConfig(t *testing.T) {
|
||||||
|
data := `
|
||||||
|
version = 2
|
||||||
|
root = "/var/lib/containerd"
|
||||||
|
`
|
||||||
|
tempDir, err := ioutil.TempDir("", "containerd_")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
path := filepath.Join(tempDir, "config.toml")
|
||||||
|
err = ioutil.WriteFile(path, []byte(data), 0600)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
var out Config
|
||||||
|
err = LoadConfig(path, &out)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
assert.Equal(t, 2, out.Version)
|
||||||
|
assert.Equal(t, "/var/lib/containerd", out.Root)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfigWithImports(t *testing.T) {
|
||||||
|
data1 := `
|
||||||
|
version = 2
|
||||||
|
root = "/var/lib/containerd"
|
||||||
|
imports = ["data2.toml"]
|
||||||
|
`
|
||||||
|
|
||||||
|
data2 := `
|
||||||
|
disabled_plugins = ["io.containerd.v1.xyz"]
|
||||||
|
`
|
||||||
|
|
||||||
|
tempDir, err := ioutil.TempDir("", "containerd_")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(filepath.Join(tempDir, "data1.toml"), []byte(data1), 0600)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(filepath.Join(tempDir, "data2.toml"), []byte(data2), 0600)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
var out Config
|
||||||
|
err = LoadConfig(filepath.Join(tempDir, "data1.toml"), &out)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 2, out.Version)
|
||||||
|
assert.Equal(t, "/var/lib/containerd", out.Root)
|
||||||
|
assert.DeepEqual(t, []string{"io.containerd.v1.xyz"}, out.DisabledPlugins)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadConfigWithCircularImports(t *testing.T) {
|
||||||
|
data1 := `
|
||||||
|
version = 2
|
||||||
|
root = "/var/lib/containerd"
|
||||||
|
imports = ["data2.toml", "data1.toml"]
|
||||||
|
`
|
||||||
|
|
||||||
|
data2 := `
|
||||||
|
disabled_plugins = ["io.containerd.v1.xyz"]
|
||||||
|
imports = ["data1.toml", "data2.toml"]
|
||||||
|
`
|
||||||
|
tempDir, err := ioutil.TempDir("", "containerd_")
|
||||||
|
assert.NilError(t, err)
|
||||||
|
defer os.RemoveAll(tempDir)
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(filepath.Join(tempDir, "data1.toml"), []byte(data1), 0600)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
err = ioutil.WriteFile(filepath.Join(tempDir, "data2.toml"), []byte(data2), 0600)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
var out Config
|
||||||
|
err = LoadConfig(filepath.Join(tempDir, "data1.toml"), &out)
|
||||||
|
assert.NilError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, 2, out.Version)
|
||||||
|
assert.Equal(t, "/var/lib/containerd", out.Root)
|
||||||
|
assert.DeepEqual(t, []string{"io.containerd.v1.xyz"}, out.DisabledPlugins)
|
||||||
|
|
||||||
|
assert.DeepEqual(t, []string{
|
||||||
|
filepath.Join(tempDir, "data1.toml"),
|
||||||
|
filepath.Join(tempDir, "data2.toml"),
|
||||||
|
}, out.Imports)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user