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`
is versioned and backwards compatible. The `version` field in the config
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.
Please use `version = 2` to enable version 2 config as version 1 has been
deprecated.
the config file then it is assumed to be a version `1` config and parsed as such.
The latest version is `version = 2`. The `main` branch is being prepared to support
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
@ -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` |
| 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 |
| 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 |

View File

@ -43,6 +43,13 @@ import (
// CurrentConfigVersion is the max config version which is supported
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.
// Config provides containerd configuration data for the server
@ -67,9 +74,11 @@ type Config struct {
Metrics MetricsConfig `toml:"metrics"`
// DisabledPlugins are IDs of plugins to disable. Disabled plugins won't be
// initialized and started.
// DisabledPlugins must use a fully qualified plugin URI.
DisabledPlugins []string `toml:"disabled_plugins"`
// RequiredPlugins are IDs of required plugins. Containerd exits if any
// 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"`
// Plugins provides plugin specific configuration for the initialization of a plugin
Plugins map[string]interface{} `toml:"plugins"`
@ -101,41 +110,83 @@ type StreamProcessor struct {
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
func (c *Config) ValidateVersion() error {
version := c.GetVersion()
if version == 1 {
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)
if c.Version > CurrentConfigVersion {
return fmt.Errorf("expected containerd config version equal to or less than `%d`, got `%d`", CurrentConfigVersion, c.Version)
}
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)
}
}
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)
}
}
for p := range c.Plugins {
if !strings.HasPrefix(p, "io.containerd.") || len(strings.SplitN(p, ".", 4)) < 4 {
return fmt.Errorf("invalid plugin key URI %q expect io.containerd.x.vx", p)
return nil
}
// 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
}
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
}

View File

@ -29,6 +29,12 @@ import (
"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) {
a := &Config{
Version: 2,
@ -215,9 +221,10 @@ version = 2
assert.Equal(t, true, pluginConfig["shim_debug"])
}
// TestDecodePluginInV1Config tests decoding non-versioned
// config (should be parsed as V1 config).
// TestDecodePluginInV1Config tests decoding non-versioned config
// (should be parsed as V1 config) and migrated to latest.
func TestDecodePluginInV1Config(t *testing.T) {
ctx := logtest.WithT(context.Background(), t)
data := `
[plugins.linux]
shim_debug = true
@ -229,5 +236,15 @@ func TestDecodePluginInV1Config(t *testing.T) {
var out Config
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
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 {
return nil, err
}
@ -203,10 +217,12 @@ func New(ctx context.Context, config *srvconfig.Config) (*Server, error) {
required[r] = struct{}{}
}
if version < srvconfig.CurrentConfigVersion {
t1 := time.Now()
// Run migration for each configuration version
// Run each plugin migration for each version to ensure that migration logic is simple and
// focused on upgrading from one version at a time.
for v := config.Version; v < srvconfig.CurrentConfigVersion; v++ {
for v := version; v < srvconfig.CurrentConfigVersion; v++ {
for _, p := range plugins {
if p.ConfigMigration != nil {
if err := p.ConfigMigration(ctx, v, config.Plugins); err != nil {
@ -215,6 +231,11 @@ func New(ctx context.Context, config *srvconfig.Config) (*Server, error) {
}
}
}
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 {
id := p.URI()