diff --git a/services/server/config/config.go b/services/server/config/config.go index 0f395f040..4d57f6cfa 100644 --- a/services/server/config/config.go +++ b/services/server/config/config.go @@ -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,44 +110,86 @@ 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 +} + // GRPCConfig provides GRPC configuration for the socket type GRPCConfig struct { Address string `toml:"address"` diff --git a/services/server/config/config_test.go b/services/server/config/config_test.go index 9932f1ecb..742af453f 100644 --- a/services/server/config/config_test.go +++ b/services/server/config/config_test.go @@ -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"]) } diff --git a/services/server/server.go b/services/server/server.go index e1c5d7384..95310b0cd 100644 --- a/services/server/server.go +++ b/services/server/server.go @@ -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,17 +217,24 @@ func New(ctx context.Context, config *srvconfig.Config) (*Server, error) { required[r] = struct{}{} } - // 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 _, p := range plugins { - if p.ConfigMigration != nil { - if err := p.ConfigMigration(ctx, v, config.Plugins); err != nil { - return nil, err + 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 := version; v < srvconfig.CurrentConfigVersion; v++ { + for _, p := range plugins { + 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 {