Merge pull request #9223 from dmcgowan/config-migration-v1

Introduce top level config migration
This commit is contained in:
Akihiro Suda 2023-10-13 08:43:13 +09:00 committed by GitHub
commit 9c367d09f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 141 additions and 36 deletions

View File

@ -351,9 +351,26 @@ We will do our best to not break compatibility in the tool in _patch_ releases.
The daemon's configuration file, commonly located in `/etc/containerd/config.toml` The daemon's configuration file, commonly located in `/etc/containerd/config.toml`
is versioned and backwards compatible. The `version` field in the config is versioned and backwards compatible. The `version` field in the config
file specifies the config's version. If no version number is specified inside file specifies the config's version. If no version number is specified inside
the config file then it is assumed to be a version 1 config and parsed as such. the config file then it is assumed to be a version `1` config and parsed as such.
Please use `version = 2` to enable version 2 config as version 1 has been The latest version is `version = 2`. The `main` branch is being prepared to support
deprecated. the next config version `3`. The configuration is automatically migrated to the
latest version on each startup, leaving the configuration file unchanged. To avoid
the migration and optimize the daemon startup time, use `containerd config migrate`
to output the configuration as the latest version. Version `1` is no longer deprecated
and is supported by migration, however, it is recommended to use at least version `2`.
Migrating a configuration to the latest version will limit the prior versions
of containerd in which the configuration can be used. It is suggested not to
migrate your configuration file until you are confident you do not need to
quickly rollback your containerd version. Use the table of configuration
versions to containerd releases to know the minimum version of containerd for
each configuration version.
| Configuration Version | Minimum containerd version |
|-----------------------|----------------------------|
| 1 | v1.0.0 |
| 2 | v1.3.0 |
| 3 | v2.0.0 |
### Not Covered ### Not Covered
@ -383,7 +400,6 @@ The deprecated features are shown in the following table:
|----------------------------------------------------------------------------------|---------------------|----------------------------|------------------------------------------| |----------------------------------------------------------------------------------|---------------------|----------------------------|------------------------------------------|
| Runtime V1 API and implementation (`io.containerd.runtime.v1.linux`) | containerd v1.4 | containerd v2.0 ✅ | Use `io.containerd.runc.v2` | | Runtime V1 API and implementation (`io.containerd.runtime.v1.linux`) | containerd v1.4 | containerd v2.0 ✅ | Use `io.containerd.runc.v2` |
| Runc V1 implementation of Runtime V2 (`io.containerd.runc.v1`) | containerd v1.4 | containerd v2.0 ✅ | Use `io.containerd.runc.v2` | | Runc V1 implementation of Runtime V2 (`io.containerd.runc.v1`) | containerd v1.4 | containerd v2.0 ✅ | Use `io.containerd.runc.v2` |
| config.toml `version = 1` | containerd v1.5 | containerd v2.0 ✅ | Use config.toml `version = 2` |
| Built-in `aufs` snapshotter | containerd v1.5 | containerd v2.0 ✅ | Use `overlayfs` snapshotter | | Built-in `aufs` snapshotter | containerd v1.5 | containerd v2.0 ✅ | Use `overlayfs` snapshotter |
| Container label `containerd.io/restart.logpath` | containerd v1.5 | containerd v2.0 ✅ | Use `containerd.io/restart.loguri` label | | Container label `containerd.io/restart.logpath` | containerd v1.5 | containerd v2.0 ✅ | Use `containerd.io/restart.loguri` label |
| `cri-containerd-*.tar.gz` release bundles | containerd v1.6 | containerd v2.0 ✅ | Use `containerd-*.tar.gz` bundles | | `cri-containerd-*.tar.gz` release bundles | containerd v1.6 | containerd v2.0 ✅ | Use `containerd-*.tar.gz` bundles |

View File

@ -43,6 +43,13 @@ import (
// CurrentConfigVersion is the max config version which is supported // CurrentConfigVersion is the max config version which is supported
const CurrentConfigVersion = 3 const CurrentConfigVersion = 3
// migrations hold the migration functions for every prior containerd config version
var migrations = []func(context.Context, *Config) error{
nil, // Version 0 is not defined, treated at version 1
v1Migrate, // Version 1 plugins renamed to URI for version 2
nil, // Version 2 has only plugin changes to version 3
}
// NOTE: Any new map fields added also need to be handled in mergeConfig. // NOTE: Any new map fields added also need to be handled in mergeConfig.
// Config provides containerd configuration data for the server // Config provides containerd configuration data for the server
@ -67,9 +74,11 @@ type Config struct {
Metrics MetricsConfig `toml:"metrics"` Metrics MetricsConfig `toml:"metrics"`
// DisabledPlugins are IDs of plugins to disable. Disabled plugins won't be // DisabledPlugins are IDs of plugins to disable. Disabled plugins won't be
// initialized and started. // initialized and started.
// DisabledPlugins must use a fully qualified plugin URI.
DisabledPlugins []string `toml:"disabled_plugins"` DisabledPlugins []string `toml:"disabled_plugins"`
// RequiredPlugins are IDs of required plugins. Containerd exits if any // RequiredPlugins are IDs of required plugins. Containerd exits if any
// required plugin doesn't exist or fails to be initialized or started. // required plugin doesn't exist or fails to be initialized or started.
// RequiredPlugins must use a fully qualified plugin URI.
RequiredPlugins []string `toml:"required_plugins"` RequiredPlugins []string `toml:"required_plugins"`
// Plugins provides plugin specific configuration for the initialization of a plugin // Plugins provides plugin specific configuration for the initialization of a plugin
Plugins map[string]interface{} `toml:"plugins"` Plugins map[string]interface{} `toml:"plugins"`
@ -101,44 +110,86 @@ type StreamProcessor struct {
Env []string `toml:"env"` Env []string `toml:"env"`
} }
// GetVersion returns the config file's version
func (c *Config) GetVersion() int {
if c.Version == 0 {
return 1
}
return c.Version
}
// ValidateVersion validates the config for a v2 file // ValidateVersion validates the config for a v2 file
func (c *Config) ValidateVersion() error { func (c *Config) ValidateVersion() error {
version := c.GetVersion() if c.Version > CurrentConfigVersion {
if version == 1 { return fmt.Errorf("expected containerd config version equal to or less than `%d`, got `%d`", CurrentConfigVersion, c.Version)
return errors.New("containerd config version `1` is no longer supported since containerd v2.0, please switch to version `2`, " +
"see https://github.com/containerd/containerd/blob/main/docs/PLUGINS.md#version-header")
}
if version > CurrentConfigVersion {
return fmt.Errorf("expected containerd config version equal to or less than `%d`, got `%d`", CurrentConfigVersion, version)
} }
for _, p := range c.DisabledPlugins { for _, p := range c.DisabledPlugins {
if !strings.HasPrefix(p, "io.containerd.") || len(strings.SplitN(p, ".", 4)) < 4 { if !strings.ContainsAny(p, ".") {
return fmt.Errorf("invalid disabled plugin URI %q expect io.containerd.x.vx", p) return fmt.Errorf("invalid disabled plugin URI %q expect io.containerd.x.vx", p)
} }
} }
for _, p := range c.RequiredPlugins { for _, p := range c.RequiredPlugins {
if !strings.HasPrefix(p, "io.containerd.") || len(strings.SplitN(p, ".", 4)) < 4 { if !strings.ContainsAny(p, ".") {
return fmt.Errorf("invalid required plugin URI %q expect io.containerd.x.vx", p) return fmt.Errorf("invalid required plugin URI %q expect io.containerd.x.vx", p)
} }
} }
for p := range c.Plugins {
if !strings.HasPrefix(p, "io.containerd.") || len(strings.SplitN(p, ".", 4)) < 4 { return nil
return fmt.Errorf("invalid plugin key URI %q expect io.containerd.x.vx", p) }
// MigrateConfig will convert the config to the latest version before using
func (c *Config) MigrateConfig(ctx context.Context) error {
for c.Version < CurrentConfigVersion {
if m := migrations[c.Version]; m != nil {
if err := m(ctx, c); err != nil {
return err
}
} }
c.Version++
} }
return nil return nil
} }
func v1Migrate(ctx context.Context, c *Config) error {
plugins := make(map[string]interface{}, len(c.Plugins))
// corePlugins is the list of used plugins before v1 was deprecated
corePlugins := map[string]string{
"cri": "io.containerd.grpc.v1.cri",
"cgroups": "io.containerd.monitor.v1.cgroups",
"linux": "io.containerd.runtime.v1.linux",
"scheduler": "io.containerd.gc.v1.scheduler",
"bolt": "io.containerd.metadata.v1.bolt",
"task": "io.containerd.runtime.v2.task",
"opt": "io.containerd.internal.v1.opt",
"restart": "io.containerd.internal.v1.restart",
"tracing": "io.containerd.internal.v1.tracing",
"otlp": "io.containerd.tracing.processor.v1.otlp",
"aufs": "io.containerd.snapshotter.v1.aufs",
"btrfs": "io.containerd.snapshotter.v1.btrfs",
"devmapper": "io.containerd.snapshotter.v1.devmapper",
"native": "io.containerd.snapshotter.v1.native",
"overlayfs": "io.containerd.snapshotter.v1.overlayfs",
"zfs": "io.containerd.snapshotter.v1.zfs",
}
for plugin, value := range c.Plugins {
if !strings.ContainsAny(plugin, ".") {
var ambiguous string
if full, ok := corePlugins[plugin]; ok {
plugin = full
} else if strings.HasSuffix(plugin, "-service") {
plugin = "io.containerd.service.v1." + plugin
} else if plugin == "windows" || plugin == "windows-lcow" {
// runtime, differ, and snapshotter plugins do not have configs for v1
ambiguous = plugin
plugin = "io.containerd.snapshotter.v1." + plugin
} else {
ambiguous = plugin
plugin = "io.containerd.grpc.v1." + plugin
}
if ambiguous != "" {
log.G(ctx).Warnf("Ambiguous %s plugin in v1 config, treating as %s", ambiguous, plugin)
}
}
plugins[plugin] = value
}
c.Plugins = plugins
return nil
}
// GRPCConfig provides GRPC configuration for the socket // GRPCConfig provides GRPC configuration for the socket
type GRPCConfig struct { type GRPCConfig struct {
Address string `toml:"address"` Address string `toml:"address"`

View File

@ -29,6 +29,12 @@ import (
"github.com/containerd/log/logtest" "github.com/containerd/log/logtest"
) )
func TestMigrations(t *testing.T) {
if len(migrations) != CurrentConfigVersion {
t.Fatalf("Migration missing, expected %d migrations, only %d defined", CurrentConfigVersion, len(migrations))
}
}
func TestMergeConfigs(t *testing.T) { func TestMergeConfigs(t *testing.T) {
a := &Config{ a := &Config{
Version: 2, Version: 2,
@ -215,9 +221,10 @@ version = 2
assert.Equal(t, true, pluginConfig["shim_debug"]) assert.Equal(t, true, pluginConfig["shim_debug"])
} }
// TestDecodePluginInV1Config tests decoding non-versioned // TestDecodePluginInV1Config tests decoding non-versioned config
// config (should be parsed as V1 config). // (should be parsed as V1 config) and migrated to latest.
func TestDecodePluginInV1Config(t *testing.T) { func TestDecodePluginInV1Config(t *testing.T) {
ctx := logtest.WithT(context.Background(), t)
data := ` data := `
[plugins.linux] [plugins.linux]
shim_debug = true shim_debug = true
@ -229,5 +236,15 @@ func TestDecodePluginInV1Config(t *testing.T) {
var out Config var out Config
err = LoadConfig(context.Background(), path, &out) err = LoadConfig(context.Background(), path, &out)
assert.ErrorContains(t, err, "config version `1` is no longer supported") assert.NoError(t, err)
assert.Equal(t, 0, out.Version)
err = out.MigrateConfig(ctx)
assert.NoError(t, err)
assert.Equal(t, 3, out.Version)
pluginConfig := map[string]interface{}{}
_, err = out.Decode(ctx, &plugin.Registration{Type: "io.containerd.runtime.v1", ID: "linux", Config: &pluginConfig})
assert.NoError(t, err)
assert.Equal(t, true, pluginConfig["shim_debug"])
} }

View File

@ -103,6 +103,20 @@ func CreateTopLevelDirectories(config *srvconfig.Config) error {
// New creates and initializes a new containerd server // New creates and initializes a new containerd server
func New(ctx context.Context, config *srvconfig.Config) (*Server, error) { func New(ctx context.Context, config *srvconfig.Config) (*Server, error) {
var (
version = config.Version
migrationT time.Duration
)
if version < srvconfig.CurrentConfigVersion {
// Migrate config to latest version
t1 := time.Now()
err := config.MigrateConfig(ctx)
if err != nil {
return nil, err
}
migrationT = time.Since(t1)
}
if err := apply(ctx, config); err != nil { if err := apply(ctx, config); err != nil {
return nil, err return nil, err
} }
@ -203,17 +217,24 @@ func New(ctx context.Context, config *srvconfig.Config) (*Server, error) {
required[r] = struct{}{} required[r] = struct{}{}
} }
// Run migration for each configuration version if version < srvconfig.CurrentConfigVersion {
// Run each plugin migration for each version to ensure that migration logic is simple and t1 := time.Now()
// focused on upgrading from one version at a time. // Run migration for each configuration version
for v := config.Version; v < srvconfig.CurrentConfigVersion; v++ { // Run each plugin migration for each version to ensure that migration logic is simple and
for _, p := range plugins { // focused on upgrading from one version at a time.
if p.ConfigMigration != nil { for v := version; v < srvconfig.CurrentConfigVersion; v++ {
if err := p.ConfigMigration(ctx, v, config.Plugins); err != nil { for _, p := range plugins {
return nil, err if p.ConfigMigration != nil {
if err := p.ConfigMigration(ctx, v, config.Plugins); err != nil {
return nil, err
}
} }
} }
} }
migrationT = migrationT + time.Since(t1)
}
if migrationT > 0 {
log.G(ctx).WithField("t", migrationT).Warnf("Configuration migrated from version %d, use `containerd config migrate` to avoid migration", version)
} }
for _, p := range plugins { for _, p := range plugins {