Merge pull request #3574 from mxpv/cfg

Support config imports
This commit is contained in:
Michael Crosby
2019-09-04 16:34:11 -04:00
committed by GitHub
13 changed files with 1230 additions and 45 deletions

View File

@@ -17,12 +17,15 @@
package config
import (
"path/filepath"
"strings"
"github.com/BurntSushi/toml"
"github.com/imdario/mergo"
"github.com/pkg/errors"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/plugin"
"github.com/pkg/errors"
)
// Config provides containerd configuration data for the server
@@ -59,16 +62,14 @@ type Config struct {
ProxyPlugins map[string]ProxyPlugin `toml:"proxy_plugins"`
// Timeouts specified as a duration
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"`
md toml.MetaData
StreamProcessors map[string]StreamProcessor `toml:"stream_processors"`
}
// StreamProcessor provides configuration for diff content processors
type StreamProcessor struct {
// ID of the processor, also used to fetch the specific payload
ID string `toml:"id"`
// Accepts specific media-types
Accepts []string `toml:"accepts"`
// Returns the media-type
@@ -202,23 +203,125 @@ func (c *Config) Decode(p *plugin.Registration) (interface{}, error) {
if !ok {
return p.Config, nil
}
if err := c.md.PrimitiveDecode(data, p.Config); err != nil {
if err := toml.PrimitiveDecode(data, p.Config); err != nil {
return nil, err
}
return p.Config, nil
}
// LoadConfig loads the containerd server config from the provided path
func LoadConfig(path string, v *Config) error {
if v == nil {
return errors.Wrapf(errdefs.ErrInvalidArgument, "argument v must not be nil")
func LoadConfig(path string, out *Config) error {
if out == 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{}
_, err := toml.DecodeFile(path, &config)
if err != nil {
return nil, err
}
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'
// "" "value" "value"
// "value" "" "value"
// 1 0 1
// 0 1 1
// []{"1"} []{"2"} []{"1","2"}
// []{"1"} []{} []{"1"}
// Maps merged by keys, but values are replaced entirely.
func mergeConfig(to, from *Config) error {
err := mergo.Merge(to, from, mergo.WithOverride, mergo.WithAppendSlice)
if err != nil {
return err
}
v.md = md
return v.ValidateV2()
// Replace entire sections instead of merging map's values.
for k, v := range from.Plugins {
to.Plugins[k] = v
}
for k, v := range from.StreamProcessors {
to.StreamProcessors[k] = v
}
for k, v := range from.ProxyPlugins {
to.ProxyPlugins[k] = v
}
return nil
}
// V1DisabledFilter matches based on ID

View File

@@ -0,0 +1,207 @@
/*
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"
"github.com/containerd/containerd/plugin"
)
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,
Timeouts: map[string]string{"a": "1"},
StreamProcessors: map[string]StreamProcessor{"1": {Path: "2", Returns: "4"}, "2": {Path: "5"}},
}
b := &Config{
Root: "new_root",
RequiredPlugins: []string{"new_plugin1", "new_plugin2"},
OOMScore: 2,
Timeouts: map[string]string{"b": "2"},
StreamProcessors: map[string]StreamProcessor{"1": {Path: "3"}},
}
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{"old_plugin", "new_plugin1", "new_plugin2"})
assert.DeepEqual(t, a.DisabledPlugins, []string{"old_plugin"})
assert.DeepEqual(t, a.Timeouts, map[string]string{"a": "1", "b": "2"})
assert.DeepEqual(t, a.StreamProcessors, map[string]StreamProcessor{"1": {Path: "3"}, "2": {Path: "5"}})
}
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"
[stream_processors]
[stream_processors."io.containerd.processor.v1.pigz"]
accepts = ["application/vnd.docker.image.rootfs.diff.tar.gzip"]
path = "unpigz"
`
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)
assert.DeepEqual(t, map[string]StreamProcessor{
"io.containerd.processor.v1.pigz": {
Accepts: []string{"application/vnd.docker.image.rootfs.diff.tar.gzip"},
Path: "unpigz",
},
}, out.StreamProcessors)
}
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)
}
func TestDecodePlugin(t *testing.T) {
data := `
version = 1
[plugins.linux]
shim_debug = true
`
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)
pluginConfig := map[string]interface{}{}
_, err = out.Decode(&plugin.Registration{ID: "linux", Config: &pluginConfig})
assert.NilError(t, err)
assert.Equal(t, true, pluginConfig["shim_debug"])
}

View File

@@ -89,8 +89,8 @@ func New(ctx context.Context, config *srvconfig.Config) (*Server, error) {
if err != nil {
return nil, err
}
for _, p := range config.StreamProcessors {
diff.RegisterProcessor(diff.BinaryHandler(p.ID, p.Returns, p.Accepts, p.Path, p.Args))
for id, p := range config.StreamProcessors {
diff.RegisterProcessor(diff.BinaryHandler(id, p.Returns, p.Accepts, p.Path, p.Args))
}
serverOpts := []grpc.ServerOption{