|
17 | 17 | package config |
18 | 18 |
|
19 | 19 | import ( |
| 20 | + "path/filepath" |
20 | 21 | "strings" |
21 | 22 |
|
22 | 23 | "github.com/BurntSushi/toml" |
| 24 | + "github.com/imdario/mergo" |
| 25 | + "github.com/pkg/errors" |
| 26 | + |
23 | 27 | "github.com/containerd/containerd/errdefs" |
24 | 28 | "github.com/containerd/containerd/plugin" |
25 | | - "github.com/pkg/errors" |
26 | 29 | ) |
27 | 30 |
|
28 | 31 | // Config provides containerd configuration data for the server |
@@ -59,16 +62,14 @@ type Config struct { |
59 | 62 | ProxyPlugins map[string]ProxyPlugin `toml:"proxy_plugins"` |
60 | 63 | // Timeouts specified as a duration |
61 | 64 | Timeouts map[string]string `toml:"timeouts"` |
| 65 | + // Imports are additional file path list to config files that can overwrite main config file fields |
| 66 | + Imports []string `toml:"imports"` |
62 | 67 |
|
63 | | - StreamProcessors []StreamProcessor `toml:"stream_processors"` |
64 | | - |
65 | | - md toml.MetaData |
| 68 | + StreamProcessors map[string]StreamProcessor `toml:"stream_processors"` |
66 | 69 | } |
67 | 70 |
|
68 | 71 | // StreamProcessor provides configuration for diff content processors |
69 | 72 | type StreamProcessor struct { |
70 | | - // ID of the processor, also used to fetch the specific payload |
71 | | - ID string `toml:"id"` |
72 | 73 | // Accepts specific media-types |
73 | 74 | Accepts []string `toml:"accepts"` |
74 | 75 | // Returns the media-type |
@@ -202,23 +203,125 @@ func (c *Config) Decode(p *plugin.Registration) (interface{}, error) { |
202 | 203 | if !ok { |
203 | 204 | return p.Config, nil |
204 | 205 | } |
205 | | - if err := c.md.PrimitiveDecode(data, p.Config); err != nil { |
| 206 | + if err := toml.PrimitiveDecode(data, p.Config); err != nil { |
206 | 207 | return nil, err |
207 | 208 | } |
208 | 209 | return p.Config, nil |
209 | 210 | } |
210 | 211 |
|
211 | 212 | // LoadConfig loads the containerd server config from the provided path |
212 | | -func LoadConfig(path string, v *Config) error { |
213 | | - if v == nil { |
214 | | - 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...) |
| 247 | + } |
| 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 | + _, err := toml.DecodeFile(path, &config) |
| 262 | + if err != nil { |
| 263 | + return nil, err |
215 | 264 | } |
216 | | - md, err := toml.DecodeFile(path, v) |
| 265 | + return config, nil |
| 266 | +} |
| 267 | + |
| 268 | +// resolveImports resolves import strings list to absolute paths list: |
| 269 | +// - If path contains *, glob pattern matching applied |
| 270 | +// - Non abs path is relative to parent config file directory |
| 271 | +// - Abs paths returned as is |
| 272 | +func resolveImports(parent string, imports []string) ([]string, error) { |
| 273 | + var out []string |
| 274 | + |
| 275 | + for _, path := range imports { |
| 276 | + if strings.Contains(path, "*") { |
| 277 | + matches, err := filepath.Glob(path) |
| 278 | + if err != nil { |
| 279 | + return nil, err |
| 280 | + } |
| 281 | + |
| 282 | + out = append(out, matches...) |
| 283 | + } else { |
| 284 | + path = filepath.Clean(path) |
| 285 | + if !filepath.IsAbs(path) { |
| 286 | + path = filepath.Join(filepath.Dir(parent), path) |
| 287 | + } |
| 288 | + |
| 289 | + out = append(out, path) |
| 290 | + } |
| 291 | + } |
| 292 | + |
| 293 | + return out, nil |
| 294 | +} |
| 295 | + |
| 296 | +// mergeConfig merges Config structs with the following rules: |
| 297 | +// 'to' 'from' 'result' |
| 298 | +// "" "value" "value" |
| 299 | +// "value" "" "value" |
| 300 | +// 1 0 1 |
| 301 | +// 0 1 1 |
| 302 | +// []{"1"} []{"2"} []{"1","2"} |
| 303 | +// []{"1"} []{} []{"1"} |
| 304 | +// Maps merged by keys, but values are replaced entirely. |
| 305 | +func mergeConfig(to, from *Config) error { |
| 306 | + err := mergo.Merge(to, from, mergo.WithOverride, mergo.WithAppendSlice) |
217 | 307 | if err != nil { |
218 | 308 | return err |
219 | 309 | } |
220 | | - v.md = md |
221 | | - return v.ValidateV2() |
| 310 | + |
| 311 | + // Replace entire sections instead of merging map's values. |
| 312 | + for k, v := range from.Plugins { |
| 313 | + to.Plugins[k] = v |
| 314 | + } |
| 315 | + |
| 316 | + for k, v := range from.StreamProcessors { |
| 317 | + to.StreamProcessors[k] = v |
| 318 | + } |
| 319 | + |
| 320 | + for k, v := range from.ProxyPlugins { |
| 321 | + to.ProxyPlugins[k] = v |
| 322 | + } |
| 323 | + |
| 324 | + return nil |
222 | 325 | } |
223 | 326 |
|
224 | 327 | // V1DisabledFilter matches based on ID |
|
0 commit comments