Skip to content

Commit a1e3779

Browse files
committed
Support config imports #3289
Signed-off-by: Maksym Pavlenko <[email protected]>
1 parent 6e2228d commit a1e3779

2 files changed

Lines changed: 262 additions & 8 deletions

File tree

services/server/config/config.go

Lines changed: 99 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,15 @@
1717
package config
1818

1919
import (
20+
"path/filepath"
2021
"strings"
2122

2223
"github.com/BurntSushi/toml"
24+
"github.com/imdario/mergo"
25+
"github.com/pkg/errors"
26+
2327
"github.com/containerd/containerd/errdefs"
2428
"github.com/containerd/containerd/plugin"
25-
"github.com/pkg/errors"
2629
)
2730

2831
// Config provides containerd configuration data for the server
@@ -57,6 +60,8 @@ type Config struct {
5760
ProxyPlugins map[string]ProxyPlugin `toml:"proxy_plugins"`
5861
// Timeouts specified as a duration
5962
Timeouts map[string]string `toml:"timeouts"`
63+
// Imports are additional file path list to config files that can overwrite main config file fields
64+
Imports []string `toml:"imports"`
6065

6166
StreamProcessors []StreamProcessor `toml:"stream_processors"`
6267

@@ -205,16 +210,102 @@ func (c *Config) Decode(p *plugin.Registration) (interface{}, error) {
205210
}
206211

207212
// LoadConfig loads the containerd server config from the provided path
208-
func LoadConfig(path string, v *Config) error {
209-
if v == nil {
210-
return errors.Wrapf(errdefs.ErrInvalidArgument, "argument v must not be nil")
213+
func LoadConfig(path string, out *Config) error {
214+
if out == nil {
215+
return errors.Wrapf(errdefs.ErrInvalidArgument, "argument out must not be nil")
216+
}
217+
218+
var (
219+
loaded = map[string]bool{}
220+
pending = []string{path}
221+
)
222+
223+
for len(pending) > 0 {
224+
path, pending = pending[0], pending[1:]
225+
226+
// Check if a file at the given path already loaded to prevent circular imports
227+
if _, ok := loaded[path]; ok {
228+
continue
229+
}
230+
231+
config, err := loadConfigFile(path)
232+
if err != nil {
233+
return err
234+
}
235+
236+
if err := mergeConfig(out, config); err != nil {
237+
return err
238+
}
239+
240+
imports, err := resolveImports(path, config.Imports)
241+
if err != nil {
242+
return err
243+
}
244+
245+
loaded[path] = true
246+
pending = append(pending, imports...)
211247
}
212-
md, err := toml.DecodeFile(path, v)
248+
249+
// Fix up the list of config files loaded
250+
out.Imports = []string{}
251+
for path := range loaded {
252+
out.Imports = append(out.Imports, path)
253+
}
254+
255+
return out.ValidateV2()
256+
}
257+
258+
// loadConfigFile decodes a TOML file at the given path
259+
func loadConfigFile(path string) (*Config, error) {
260+
config := &Config{}
261+
md, err := toml.DecodeFile(path, &config)
213262
if err != nil {
214-
return err
263+
return nil, err
215264
}
216-
v.md = md
217-
return v.ValidateV2()
265+
config.md = md
266+
return config, nil
267+
}
268+
269+
// resolveImports resolves import strings list to absolute paths list:
270+
// - If path contains *, glob pattern matching applied
271+
// - Non abs path is relative to parent config file directory
272+
// - Abs paths returned as is
273+
func resolveImports(parent string, imports []string) ([]string, error) {
274+
var out []string
275+
276+
for _, path := range imports {
277+
if strings.Contains(path, "*") {
278+
matches, err := filepath.Glob(path)
279+
if err != nil {
280+
return nil, err
281+
}
282+
283+
out = append(out, matches...)
284+
} else {
285+
path = filepath.Clean(path)
286+
if !filepath.IsAbs(path) {
287+
path = filepath.Join(filepath.Dir(parent), path)
288+
}
289+
290+
out = append(out, path)
291+
}
292+
}
293+
294+
return out, nil
295+
}
296+
297+
// mergeConfig merges Config structs with the following rules:
298+
// 'to' 'from' 'result' overwrite?
299+
// "" "value" "value" yes
300+
// "value" "" "value" no
301+
// 1 0 1 no
302+
// 0 1 1 yes
303+
// []{"1"} []{"2"} []{"2"} yes
304+
// []{"1"} []{} []{"1"} no
305+
func mergeConfig(to, from *Config) error {
306+
return mergo.Merge(to, from, func(config *mergo.Config) {
307+
config.Overwrite = true
308+
})
218309
}
219310

220311
// V1DisabledFilter matches based on ID
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package config
18+
19+
import (
20+
"io/ioutil"
21+
"os"
22+
"path/filepath"
23+
"testing"
24+
25+
"gotest.tools/assert"
26+
)
27+
28+
func TestMergeConfigs(t *testing.T) {
29+
a := &Config{
30+
Version: 2,
31+
Root: "old_root",
32+
RequiredPlugins: []string{"old_plugin"},
33+
DisabledPlugins: []string{"old_plugin"},
34+
State: "old_state",
35+
OOMScore: 1,
36+
}
37+
38+
b := &Config{
39+
Root: "new_root",
40+
RequiredPlugins: []string{"new_plugin1", "new_plugin2"},
41+
OOMScore: 2,
42+
}
43+
44+
err := mergeConfig(a, b)
45+
assert.NilError(t, err)
46+
47+
assert.Equal(t, a.Version, 2)
48+
assert.Equal(t, a.Root, "new_root")
49+
assert.Equal(t, a.State, "old_state")
50+
assert.Equal(t, a.OOMScore, 2)
51+
assert.DeepEqual(t, a.RequiredPlugins, []string{"new_plugin1", "new_plugin2"})
52+
assert.DeepEqual(t, a.DisabledPlugins, []string{"old_plugin"})
53+
}
54+
55+
func TestResolveImports(t *testing.T) {
56+
tempDir, err := ioutil.TempDir("", "containerd_")
57+
assert.NilError(t, err)
58+
defer os.RemoveAll(tempDir)
59+
60+
for _, filename := range []string{"config_1.toml", "config_2.toml", "test.toml"} {
61+
err = ioutil.WriteFile(filepath.Join(tempDir, filename), []byte(""), 0600)
62+
assert.NilError(t, err)
63+
}
64+
65+
imports, err := resolveImports(filepath.Join(tempDir, "root.toml"), []string{
66+
filepath.Join(tempDir, "config_*.toml"), // Glob
67+
filepath.Join(tempDir, "./test.toml"), // Path clean up
68+
"current.toml", // Resolve current working dir
69+
})
70+
assert.NilError(t, err)
71+
72+
assert.DeepEqual(t, imports, []string{
73+
filepath.Join(tempDir, "config_1.toml"),
74+
filepath.Join(tempDir, "config_2.toml"),
75+
filepath.Join(tempDir, "test.toml"),
76+
filepath.Join(tempDir, "current.toml"),
77+
})
78+
}
79+
80+
func TestLoadSingleConfig(t *testing.T) {
81+
data := `
82+
version = 2
83+
root = "/var/lib/containerd"
84+
`
85+
tempDir, err := ioutil.TempDir("", "containerd_")
86+
assert.NilError(t, err)
87+
defer os.RemoveAll(tempDir)
88+
89+
path := filepath.Join(tempDir, "config.toml")
90+
err = ioutil.WriteFile(path, []byte(data), 0600)
91+
assert.NilError(t, err)
92+
93+
var out Config
94+
err = LoadConfig(path, &out)
95+
assert.NilError(t, err)
96+
assert.Equal(t, 2, out.Version)
97+
assert.Equal(t, "/var/lib/containerd", out.Root)
98+
}
99+
100+
func TestLoadConfigWithImports(t *testing.T) {
101+
data1 := `
102+
version = 2
103+
root = "/var/lib/containerd"
104+
imports = ["data2.toml"]
105+
`
106+
107+
data2 := `
108+
disabled_plugins = ["io.containerd.v1.xyz"]
109+
`
110+
111+
tempDir, err := ioutil.TempDir("", "containerd_")
112+
assert.NilError(t, err)
113+
defer os.RemoveAll(tempDir)
114+
115+
err = ioutil.WriteFile(filepath.Join(tempDir, "data1.toml"), []byte(data1), 0600)
116+
assert.NilError(t, err)
117+
118+
err = ioutil.WriteFile(filepath.Join(tempDir, "data2.toml"), []byte(data2), 0600)
119+
assert.NilError(t, err)
120+
121+
var out Config
122+
err = LoadConfig(filepath.Join(tempDir, "data1.toml"), &out)
123+
assert.NilError(t, err)
124+
125+
assert.Equal(t, 2, out.Version)
126+
assert.Equal(t, "/var/lib/containerd", out.Root)
127+
assert.DeepEqual(t, []string{"io.containerd.v1.xyz"}, out.DisabledPlugins)
128+
}
129+
130+
func TestLoadConfigWithCircularImports(t *testing.T) {
131+
data1 := `
132+
version = 2
133+
root = "/var/lib/containerd"
134+
imports = ["data2.toml", "data1.toml"]
135+
`
136+
137+
data2 := `
138+
disabled_plugins = ["io.containerd.v1.xyz"]
139+
imports = ["data1.toml", "data2.toml"]
140+
`
141+
tempDir, err := ioutil.TempDir("", "containerd_")
142+
assert.NilError(t, err)
143+
defer os.RemoveAll(tempDir)
144+
145+
err = ioutil.WriteFile(filepath.Join(tempDir, "data1.toml"), []byte(data1), 0600)
146+
assert.NilError(t, err)
147+
148+
err = ioutil.WriteFile(filepath.Join(tempDir, "data2.toml"), []byte(data2), 0600)
149+
assert.NilError(t, err)
150+
151+
var out Config
152+
err = LoadConfig(filepath.Join(tempDir, "data1.toml"), &out)
153+
assert.NilError(t, err)
154+
155+
assert.Equal(t, 2, out.Version)
156+
assert.Equal(t, "/var/lib/containerd", out.Root)
157+
assert.DeepEqual(t, []string{"io.containerd.v1.xyz"}, out.DisabledPlugins)
158+
159+
assert.DeepEqual(t, []string{
160+
filepath.Join(tempDir, "data1.toml"),
161+
filepath.Join(tempDir, "data2.toml"),
162+
}, out.Imports)
163+
}

0 commit comments

Comments
 (0)