Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,7 @@ The deprecated features are shown in the following table:
| CRI `v1alpha2` | containerd v1.7 | containerd v2.0 ✅ | Use CRI `v1` |
| Legacy CRI implementation of podsandbox support | containerd v2.0 | containerd v2.0 ✅ | |
| Go-Plugin library (`*.so`) as containerd runtime plugin | containerd v2.0 | containerd v2.1 | Use external plugins (proxy or binary) |
| CNI `bin_dir` in CRI runtime config (`plugins.'io.containerd.cri.v1.runtime'.cni.bin_dir`) | containerd v2.1 | containerd v2.2 | Change `bin_dir` to `bin_dirs` in the same section which supports a list of directories |

- Pulling Schema 1 images has been disabled in containerd v2.0, but it still can be enabled by setting an environment variable `CONTAINERD_ENABLE_DEPRECATED_PULL_SCHEMA_1_IMAGE=1`
until containerd v2.1. `ctr` users have to specify `--local` too (e.g., `ctr images pull --local`). Users of CRI clients (such as Kubernetes and `crictl`) have to specify this environment variable on the containerd daemon (usually in the systemd unit).
Expand Down
4 changes: 3 additions & 1 deletion docs/cri/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,9 @@ version = 3
ShimCgroup = ''

[plugins.'io.containerd.cri.v1.runtime'.cni]
bin_dir = '/opt/cni/bin'
# DEPRECATED, use `bin_dirs` instead (since containerd v2.1).
bin_dir = ''
bin_dirs = ['/opt/cni/bin']
conf_dir = '/etc/cni/net.d'
max_conf_num = 1
setup_serially = false
Expand Down
29 changes: 29 additions & 0 deletions internal/cri/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"errors"
"fmt"
"net/url"
"slices"
"time"

"github.com/containerd/log"
Expand Down Expand Up @@ -152,7 +153,13 @@ type ContainerdConfig struct {
// CniConfig contains toml config related to cni
type CniConfig struct {
// NetworkPluginBinDir is the directory in which the binaries for the plugin is kept.
//
// DEPRECATED: use `NetworkPluginBinDirs` instead.`
NetworkPluginBinDir string `toml:"bin_dir" json:"binDir"`
// NetworkPluginBinDirs is the directories in which the binaries for the plugin is kept.
//
// Only use one of NetworkPluginBinDir and NetworkPluginBinDirs, not both.
NetworkPluginBinDirs []string `toml:"bin_dirs" json:"binDirs"`
// NetworkPluginConfDir is the directory in which the admin places a CNI conf.
NetworkPluginConfDir string `toml:"conf_dir" json:"confDir"`
// NetworkPluginMaxConfNum is the max number of plugin config files that will
Expand Down Expand Up @@ -528,6 +535,28 @@ func ValidateRuntimeConfig(ctx context.Context, c *RuntimeConfig) ([]deprecation
return warnings, fmt.Errorf("no corresponding runtime configured in `containerd.runtimes` for `containerd` `default_runtime_name = \"%s\"", c.ContainerdConfig.DefaultRuntimeName)
}

// Validation for CNI config
if len(c.CniConfig.NetworkPluginBinDir) != 0 {
warnings = append(warnings, deprecation.CRICNIBinDir)
log.G(ctx).Warning("`bin_dir` is deprecated, please use `bin_dirs` instead")

if slices.Equal(c.CniConfig.NetworkPluginBinDirs, defaultNetworkPluginBinDirs()) {
// if a user set `bin_dir` explicitly, we remove the default value of `bin_dirs`
// to avoid the unexpected conflict between the two since we don't allow setting both.
c.CniConfig.NetworkPluginBinDirs = nil
}
if len(c.CniConfig.NetworkPluginBinDirs) == 0 {
// Before `NetworkPluginBinDir` is deprecated and removed, we manually move it
// into `NetworkPluginBinDirs` (if `NetworkPluginBinDirs` is empty)
// so that we can use it in the rest of the code.
c.CniConfig.NetworkPluginBinDirs = []string{c.CniConfig.NetworkPluginBinDir}
c.CniConfig.NetworkPluginBinDir = ""
}
}
if len(c.CniConfig.NetworkPluginBinDirs) != 0 && len(c.CniConfig.NetworkPluginBinDir) != 0 {
return warnings, errors.New("`cni.bin_dir` and `cni.bin_dirs` cannot be set at the same time")
Comment thread
mikebrow marked this conversation as resolved.
}

for k, r := range c.ContainerdConfig.Runtimes {
if r.CgroupWritable && !opts.IsCgroup2UnifiedMode() {
return warnings, fmt.Errorf("runtime %s: `cgroup_writable` is only supported on cgroup v2", k)
Expand Down
44 changes: 44 additions & 0 deletions internal/cri/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,49 @@ func TestValidateConfig(t *testing.T) {
},
runtimeExpectedErr: "no corresponding runtime configured in `containerd.runtimes` for `containerd` `default_runtime_name = \"default\"",
},
"specify both cni.bin_dir and cni_bin_dirs": {
runtimeConfig: &RuntimeConfig{
ContainerdConfig: ContainerdConfig{
DefaultRuntimeName: RuntimeDefault,
Runtimes: map[string]Runtime{
RuntimeDefault: {},
},
},
CniConfig: CniConfig{
NetworkPluginBinDir: "/opt/mycni/bin",
NetworkPluginBinDirs: []string{"/opt/mycni/bin"},
},
},
runtimeExpectedErr: "`cni.bin_dir` and `cni.bin_dirs` cannot be set at the same time",
warnings: []deprecation.Warning{deprecation.CRICNIBinDir},
},
"cni.bin_dir, if used, is moved to cni.bin_dirs": {
runtimeConfig: &RuntimeConfig{
ContainerdConfig: ContainerdConfig{
DefaultRuntimeName: RuntimeDefault,
Runtimes: map[string]Runtime{
RuntimeDefault: {},
},
},
CniConfig: CniConfig{
NetworkPluginBinDir: "/opt/mycni/bin",
},
},
runtimeExpected: &RuntimeConfig{
ContainerdConfig: ContainerdConfig{
DefaultRuntimeName: RuntimeDefault,
Runtimes: map[string]Runtime{
RuntimeDefault: {
Sandboxer: string(ModePodSandbox),
},
},
},
CniConfig: CniConfig{
NetworkPluginBinDirs: []string{"/opt/mycni/bin"},
},
},
warnings: []deprecation.Warning{deprecation.CRICNIBinDir},
},

"deprecated auths": {
runtimeConfig: &RuntimeConfig{
Expand Down Expand Up @@ -259,6 +302,7 @@ func TestValidateConfig(t *testing.T) {
if test.runtimeConfig != nil {
w, err := ValidateRuntimeConfig(context.Background(), test.runtimeConfig)
if test.runtimeExpectedErr != "" {
assert.Error(t, err)
assert.Contains(t, err.Error(), test.runtimeExpectedErr)
} else {
assert.NoError(t, err)
Expand Down
6 changes: 5 additions & 1 deletion internal/cri/config/config_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import (
"github.com/pelletier/go-toml/v2"
)

func defaultNetworkPluginBinDirs() []string {
return []string{"/opt/cni/bin"}
}

func DefaultImageConfig() ImageConfig {
return ImageConfig{
Snapshotter: defaults.DefaultSnapshotter,
Expand Down Expand Up @@ -72,7 +76,7 @@ func DefaultRuntimeConfig() RuntimeConfig {

return RuntimeConfig{
CniConfig: CniConfig{
NetworkPluginBinDir: "/opt/cni/bin",
NetworkPluginBinDirs: defaultNetworkPluginBinDirs(),
NetworkPluginConfDir: "/etc/cni/net.d",
NetworkPluginMaxConfNum: 1, // only one CNI plugin config file will be loaded
NetworkPluginSetupSerially: false,
Expand Down
6 changes: 5 additions & 1 deletion internal/cri/config/config_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ import (
"github.com/containerd/containerd/v2/defaults"
)

func defaultNetworkPluginBinDirs() []string {
return []string{filepath.Join(os.Getenv("ProgramFiles"), "containerd", "cni", "bin")}
}

func DefaultImageConfig() ImageConfig {
return ImageConfig{
Snapshotter: defaults.DefaultSnapshotter,
Expand All @@ -42,7 +46,7 @@ func DefaultImageConfig() ImageConfig {
func DefaultRuntimeConfig() RuntimeConfig {
return RuntimeConfig{
CniConfig: CniConfig{
NetworkPluginBinDir: filepath.Join(os.Getenv("ProgramFiles"), "containerd", "cni", "bin"),
NetworkPluginBinDirs: defaultNetworkPluginBinDirs(),
NetworkPluginConfDir: filepath.Join(os.Getenv("ProgramFiles"), "containerd", "cni", "conf"),
NetworkPluginMaxConfNum: 1,
NetworkPluginSetupSerially: false,
Expand Down
2 changes: 1 addition & 1 deletion internal/cri/server/service_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func (c *criService) initPlatform() (err error) {
i, err := cni.New(cni.WithMinNetworkCount(networkAttachCount),
cni.WithPluginConfDir(dir),
cni.WithPluginMaxConfNum(max),
cni.WithPluginDir([]string{c.config.NetworkPluginBinDir}))
cni.WithPluginDir(c.config.NetworkPluginBinDirs))
Copy link
Copy Markdown
Member Author

@djdongjin djdongjin Mar 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mikebrow actually I just realized one thing: we cannot switch the default here yet, as the old field is not yet removed.

Say if a user only uses bin_dir, we will actually set this default bin_dirs, causing both fields being used and fail our validation. (it's kind of similar to we cannot set the default config_path yet as we haven't removed the deprecated mirros. #6488).

Giving this, how about let's keep the default value unchanged? And do the switch when we remove the old field? (we will still add a deprecation warning saying bin_dir is deprecated)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait... actually the opposite also stands. If we keep the default bin_dir unchanged. If a user set up bin_dirs instead. We will end up in the same situation 😅

Copy link
Copy Markdown
Member Author

@djdongjin djdongjin Mar 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

essentially we cannot figure out a field is set by default or explicitly by user.

We can do something like this:

  1. move the default to bin_dirs here (keep this change).
  2. if bin_dir is not empty, log a deprecation warning. then check if len(bin_dirs) == 1 && bin_dirs[0] == "/opt/cni/bin"
    2.1. if yes, that means bin_dirs is from default and we can proceeed by merging the two.
    2.2. if no, that means the user set up both bin_dir and bin_dirs, we should error in this case.

WDYT @mikebrow

Copy link
Copy Markdown
Member

@mikebrow mikebrow Mar 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the way it works.. just to confirm...
if the config.toml is unset (file is not present in the path) we go to the compiled in defaults.
if the config.toml is set we use what we get from the config.toml (no matter what version is used as defined in the config) and migrate to the latest version
if the request is made to generate the config defaults as output containerd config default we output that

the concern about "default" should not be an issue unless the field doesn't work when unset... but even then we should not be .. "merging" defaults over unset fields with the exception of there is no config.toml at all

Copy link
Copy Markdown
Member

@mikebrow mikebrow Mar 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so .. if bin_dir is set in a prior version.. migrate to bin_dirs[] to become the current version ..

pls check my math :-) may require a version bump for the config for 2.1

Copy link
Copy Markdown
Member Author

@djdongjin djdongjin Mar 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so .. if bin_dir is set in a prior version.. migrate to bin_dirs[] to become the current version

yep that's right, no issue if a user migrates from verson 2 to 3, as bin_dirs doesn't exist there. but the issue is in latest config version 3, we have both bin_dir and bin_dirs. And there is no migration if version is already latest.

Say a user has bin_dir = /my/cni/bin in their version=3 config. Previously it will just overwrite the bin_dir's default value.

Since we move default from bin_dir to bin_dirs, now the user has below at the end of config loading:

bin_dir = /my/cni/bin`     // from user's config
bin_dirs = ['opt/cni/bin'] // from our new default.

This will break our config validation which doesn't allow both fields non-empty. To the user it shouldn't break, becault they still only set up bin_dir only. :)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if the config.toml is set we use what we get from the config.toml (no matter what version is used as defined in the config) and migrate to the latest version

It will be:

  1. migrate toml content first
  2. get the default config value
  3. insert toml content into the config struct (e.g., overwrite default values)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so .. if bin_dir is set in a prior version.. migrate to bin_dirs[] to become the current version

yep that's right, no issue if a user migrates from verson 2 to 3, as bin_dirs doesn't exist there. but the issue is in latest config version 3, we have both bin_dir and bin_dirs. And there is no migration if version is already latest.

Say a user has bin_dir = /my/cni/bin in their version=3 config. Previously it will just overwrite the bin_dir's default value.

Since we move default from bin_dir to bin_dirs, now the user has below at the end of config loading:

bin_dir = /my/cni/bin`     // from user's config
bin_dirs = ['opt/cni/bin'] // from our new default.

This will break our config validation which doesn't allow both fields non-empty. To the user it shouldn't break, becault they still only set up bin_dir only. :)

nod.. needs a new config "version" to migrate to otherwise user will have to do a manual migration.. to 3.0 containerd 2.0 to 3.0++ containerd 2.1 .. surprised we didn't do a bump yet missed we don't have a 4.0 config for 2.1 or some other process to handle new fields minor migrations for exclusive fields.. @samuelkarp @dmcgowan ... so hold off on merging till we get a migration solution from 2.0 to 2.1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we could hold off on the migration feature until there is a bump.. keeping it a "manual" operation .. leave the default as it was.. originally.. add the new field description in to the docs with the deprecation.. indicating the old field will be migrated with the next version of the config.. something to that effect

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mikebrow could you PTAL the latest patch? especially this file: https://github.com/containerd/containerd/pull/11311/files#diff-e8d6dbb2c75752c173c570eabb9961d5b0f3273b682788f2d7eab2f5ffd5d7d6

This should handle the case where a user-set bin_dir unexpectedly conflict with the default bin_dirs.

I think this will make the change backward compatible with 2.0, i.e., regardless if user set bin_dir or not, there is no behavior difference. And when user uses this new bin_dirs, we can still enforce the restriction that you cannot set bin_dir/bin_dirs at the same time.

if err != nil {
return fmt.Errorf("failed to initialize cni: %w", err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/cri/server/service_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func (c *criService) initPlatform() error {
i, err := cni.New(cni.WithMinNetworkCount(windowsNetworkAttachCount),
cni.WithPluginConfDir(dir),
cni.WithPluginMaxConfNum(max),
cni.WithPluginDir([]string{c.config.NetworkPluginBinDir}))
cni.WithPluginDir(c.config.NetworkPluginBinDirs))
if err != nil {
return fmt.Errorf("failed to initialize cni: %w", err)
}
Expand Down
4 changes: 4 additions & 0 deletions pkg/deprecation/deprecation.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const (
CRIRegistryAuths Warning = Prefix + "cri-registry-auths"
// CRIRegistryConfigs is a warning for the use of the `configs` property
CRIRegistryConfigs Warning = Prefix + "cri-registry-configs"
// CRICNIBinDir is a warning for the use of the `bin_dir` property
CRICNIBinDir = Prefix + "cri-cni-bin-dir"
// OTLPTracingConfig is a warning for the use of the `otlp` property
TracingOTLPConfig Warning = Prefix + "tracing-processor-config"
// TracingServiceConfig is a warning for the use of the `tracing` property
Expand All @@ -52,6 +54,8 @@ var messages = map[Warning]string{
"Use `ImagePullSecrets` instead.",
CRIRegistryConfigs: "The `configs` property of `[plugins.\"io.containerd.grpc.v1.cri\".registry]` is deprecated since containerd v1.5 and will be removed in containerd v2.1." +
"Use `config_path` instead.",
CRICNIBinDir: "The `bin_dir` property of `[plugins.\"io.containerd.cri.v1.runtime\".cni`] is deprecated since containerd v2.1 and will be removed in containerd v2.2. " +
"Use `bin_dirs` in the same section instead.",

TracingOTLPConfig: "The `otlp` property of `[plugins.\"io.containerd.tracing.processor.v1\".otlp]` is deprecated since containerd v1.6 and will be removed in containerd v2.0." +
"Use OTLP environment variables instead: https://opentelemetry.io/docs/specs/otel/protocol/exporter/",
Expand Down
25 changes: 25 additions & 0 deletions plugins/cri/runtime/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,9 @@ func configMigration(ctx context.Context, configVersion int, pluginConfigs map[s
func migrateConfig(dst, src map[string]interface{}) {
for k, v := range src {
switch k {
case "cni":
// skip (handled separately below)
continue
case "containerd":
// skip (handled separately below)
continue
Expand Down Expand Up @@ -235,6 +238,28 @@ func migrateConfig(dst, src map[string]interface{}) {
}
}

if cniConf, ok := src["cni"].(map[string]interface{}); ok {
newCniConf, ok := dst["cni"].(map[string]interface{})
if !ok {
newCniConf = map[string]interface{}{}
}
for k, v := range cniConf {
switch k {
case "bin_dir":
// migrate `bin_dir` to `bin_dirs` only if `bin_dirs`
// is not already set
if binDirs, ok := newCniConf["bin_dirs"].([]string); !ok || len(binDirs) == 0 {
newCniConf["bin_dirs"] = []string{v.(string)}
}
default:
if _, ok := newCniConf[k]; !ok {
newCniConf[k] = v
}
}
}
dst["cni"] = newCniConf
}

// migrate cri containerd configs
containerdConf, ok := src["containerd"].(map[string]interface{})
if !ok {
Expand Down
12 changes: 12 additions & 0 deletions plugins/cri/runtime/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (

func TestCRIRuntimePluginConfigMigration(t *testing.T) {
runcSandboxer := "podsandbox"
cniBinDir := "/opt/cni/bin"

grpcCri := map[string]interface{}{
"enable_selinux": true,
Expand All @@ -41,6 +42,9 @@ func TestCRIRuntimePluginConfigMigration(t *testing.T) {
},
},
},
"cni": map[string]interface{}{
"bin_dir": cniBinDir,
},
}

pluginConfigs := map[string]interface{}{
Expand All @@ -66,4 +70,12 @@ func TestCRIRuntimePluginConfigMigration(t *testing.T) {
require.NotNil(t, runc)
assert.Equal(t, runcSandboxer, runc["sandboxer"])
assert.NotContains(t, runc, "sandbox_mode")

cni, ok := runtimeConf["cni"].(map[string]interface{})
require.True(t, ok)
require.NotNil(t, cni)
cniBinDirs, ok := cni["bin_dirs"].([]string)
require.True(t, ok)
require.Len(t, cniBinDirs, 1)
require.Equal(t, cniBinDir, cniBinDirs[0])
}