From 1077aa4b12beb5e760ee7f24a7596da526af7926 Mon Sep 17 00:00:00 2001 From: Sri Harsha Date: Fri, 23 Oct 2026 20:40:25 +0000 Subject: [PATCH 1/4] aks-node-controller/parser/containerd_test.go --- aks-node-controller/parser/containerd_test.go | 102 ++++++++++++++++++ .../parser/templates/containerd.toml.gtpl | 8 ++ .../templates/containerd_no_GPU.toml.gtpl | 8 ++ .../parser/templates/containerd_v2.toml.gtpl | 8 ++ .../templates/containerd_v2_no_GPU.toml.gtpl | 8 ++ .../aksnodeconfig/v1/containerd_config.pb.go | 49 +++++++-- .../pkg/nodeconfigutils/containerd_test.go | 60 +++++++++++ .../pkg/nodeconfigutils/validation.go | 17 +++ .../aksnodeconfig/v1/containerd_config.proto | 8 ++ 9 files changed, 260 insertions(+), 8 deletions(-) create mode 100644 aks-node-controller/parser/containerd_test.go create mode 100644 aks-node-controller/pkg/nodeconfigutils/containerd_test.go diff --git a/aks-node-controller/parser/containerd_test.go b/aks-node-controller/parser/containerd_test.go new file mode 100644 index 00000000000..fe10804d92c --- /dev/null +++ b/aks-node-controller/parser/containerd_test.go @@ -0,0 +1,102 @@ +package parser + +import ( + "context" + "encoding/base64" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + "testing" + + aksnodeconfigv1 "github.com/Azure/agentbaker/aks-node-controller/pkg/gen/aksnodeconfig/v1" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestContainerdImagePullTemplates(t *testing.T) { + for _, version := range []string{"1.7.22", "2.2.4"} { + for _, noGPU := range []bool{false, true} { + for _, tc := range []struct { + name string + settings *aksnodeconfigv1.ContainerdConfig + }{ + {"absent", nil}, + {"empty", &aksnodeconfigv1.ContainerdConfig{}}, + {"false", &aksnodeconfigv1.ContainerdConfig{ImagePullWithSyncFs: proto.Bool(false)}}, + {"true", &aksnodeconfigv1.ContainerdConfig{ImagePullWithSyncFs: proto.Bool(true)}}, + {"zero timeout", &aksnodeconfigv1.ContainerdConfig{ImagePullProgressTimeout: proto.String("0s")}}, + {"both", &aksnodeconfigv1.ContainerdConfig{ImagePullWithSyncFs: proto.Bool(true), ImagePullProgressTimeout: proto.String("1m30s")}}, + } { + t.Run(fmt.Sprintf("%s/noGPU=%t/%s", version, noGPU, tc.name), func(t *testing.T) { + cfg := &aksnodeconfigv1.Configuration{ + KubernetesVersion: "1.34.0", + GpuConfig: &aksnodeconfigv1.GpuConfig{EnableNvidia: proto.Bool(true)}, + ContainerdConfig: tc.settings, + } + rendered, err := containerdConfigFromAKSNodeConfig(cfg, noGPU, version) + require.NoError(t, err) + plugin := "io.containerd.grpc.v1.cri" + if isContainerdV2(version) { + plugin = "io.containerd.cri.v1.images" + } + header := `[plugins."` + plugin + `"]` + "\n" + start := strings.Index(rendered, header) + require.NotEqual(t, -1, start) + section := rendered[start+len(header):] + if next := regexp.MustCompile(`(?m)^[ \t]*\[`).FindStringIndex(section); next != nil { + section = section[:next[0]] + } + if tc.settings != nil && tc.settings.ImagePullWithSyncFs != nil { + require.Contains(t, section, fmt.Sprintf(" image_pull_with_sync_fs = %t\n", tc.settings.GetImagePullWithSyncFs())) + require.Equal(t, 1, strings.Count(rendered, "image_pull_with_sync_fs")) + } else { + require.NotContains(t, rendered, "image_pull_with_sync_fs") + } + if tc.settings != nil && tc.settings.ImagePullProgressTimeout != nil { + require.Contains(t, section, fmt.Sprintf(" image_pull_progress_timeout = %q\n", tc.settings.GetImagePullProgressTimeout())) + require.Equal(t, 1, strings.Count(rendered, "image_pull_progress_timeout")) + } else { + require.NotContains(t, rendered, "image_pull_progress_timeout") + } + if tc.settings == nil || (tc.settings.ImagePullWithSyncFs == nil && tc.settings.ImagePullProgressTimeout == nil) { + cfg.ContainerdConfig = nil + baseline, err := containerdConfigFromAKSNodeConfig(cfg, noGPU, version) + require.NoError(t, err) + require.Equal(t, baseline, rendered) + } + }) + } + } + } +} + +func TestBuildCSECmdContainerdImagePullSettings(t *testing.T) { + for _, version := range []string{"1.7.22", "2.2.4"} { + t.Run(version, func(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "containerd"), []byte(fmt.Sprintf(`#!/bin/sh +[ "$1" = "--version" ] || exit 1 +echo "containerd containerd.io %s test" +`, version)), 0755)) + t.Setenv("PATH", dir) + cfg := &aksnodeconfigv1.Configuration{ContainerdConfig: &aksnodeconfigv1.ContainerdConfig{ + ImagePullWithSyncFs: proto.Bool(false), + ImagePullProgressTimeout: proto.String("0s"), + }} + cmd, err := BuildCSECmd(context.Background(), cfg, nil) + require.NoError(t, err) + env := environToMap(cmd.Env) + for _, key := range []string{"CONTAINERD_CONFIG_CONTENT", "CONTAINERD_CONFIG_NO_GPU_CONTENT"} { + content, err := base64.StdEncoding.DecodeString(env[key]) + require.NoError(t, err) + require.Contains(t, string(content), "image_pull_with_sync_fs = false") + require.Contains(t, string(content), `image_pull_progress_timeout = "0s"`) + } + require.NotContains(t, env, "ANC_CONFIGURE_CONTAINERD_BINARY") + require.NotContains(t, env, "CONTAINERD_IMAGE_PULL_WITH_SYNC_FS") + require.NotContains(t, env, "CONTAINERD_IMAGE_PULL_PROGRESS_TIMEOUT") + }) + } +} diff --git a/aks-node-controller/parser/templates/containerd.toml.gtpl b/aks-node-controller/parser/templates/containerd.toml.gtpl index a2e4b1a26a9..3dd0be790eb 100644 --- a/aks-node-controller/parser/templates/containerd.toml.gtpl +++ b/aks-node-controller/parser/templates/containerd.toml.gtpl @@ -15,6 +15,14 @@ root = "{{.KubeletConfig.GetContainerDataDir}}"{{- end}} enable_tar_index = false {{- end}} [plugins."io.containerd.grpc.v1.cri"] +{{- with .GetContainerdConfig }} + {{- if ne .ImagePullWithSyncFs nil }} + image_pull_with_sync_fs = {{ .GetImagePullWithSyncFs }} + {{- end }} + {{- if ne .ImagePullProgressTimeout nil }} + image_pull_progress_timeout = {{ printf "%q" .GetImagePullProgressTimeout }} + {{- end }} +{{- end }} sandbox_image = "{{ .KubeBinaryConfig.GetPodInfraContainerImageUrl }}" enable_cdi = true [plugins."io.containerd.grpc.v1.cri".containerd] diff --git a/aks-node-controller/parser/templates/containerd_no_GPU.toml.gtpl b/aks-node-controller/parser/templates/containerd_no_GPU.toml.gtpl index 1addca22cdb..71c9435ecdb 100644 --- a/aks-node-controller/parser/templates/containerd_no_GPU.toml.gtpl +++ b/aks-node-controller/parser/templates/containerd_no_GPU.toml.gtpl @@ -15,6 +15,14 @@ root = "{{.KubeletConfig.GetContainerDataDir}}"{{- end}} enable_tar_index = false {{- end}} [plugins."io.containerd.grpc.v1.cri"] +{{- with .GetContainerdConfig }} + {{- if ne .ImagePullWithSyncFs nil }} + image_pull_with_sync_fs = {{ .GetImagePullWithSyncFs }} + {{- end }} + {{- if ne .ImagePullProgressTimeout nil }} + image_pull_progress_timeout = {{ printf "%q" .GetImagePullProgressTimeout }} + {{- end }} +{{- end }} sandbox_image = "{{ .KubeBinaryConfig.GetPodInfraContainerImageUrl }}" [plugins."io.containerd.grpc.v1.cri".containerd] {{- if .GetIsKata }} diff --git a/aks-node-controller/parser/templates/containerd_v2.toml.gtpl b/aks-node-controller/parser/templates/containerd_v2.toml.gtpl index d99504ac7e5..195b58fa1d6 100644 --- a/aks-node-controller/parser/templates/containerd_v2.toml.gtpl +++ b/aks-node-controller/parser/templates/containerd_v2.toml.gtpl @@ -15,6 +15,14 @@ root = "{{.KubeletConfig.GetContainerDataDir}}"{{- end}} enable_tar_index = false {{- end}} [plugins."io.containerd.cri.v1.images"] +{{- with .GetContainerdConfig }} + {{- if ne .ImagePullWithSyncFs nil }} + image_pull_with_sync_fs = {{ .GetImagePullWithSyncFs }} + {{- end }} + {{- if ne .ImagePullProgressTimeout nil }} + image_pull_progress_timeout = {{ printf "%q" .GetImagePullProgressTimeout }} + {{- end }} +{{- end }} {{- if .GetEnableArtifactStreaming }} snapshotter = "overlaybd" disable_snapshot_annotations = false diff --git a/aks-node-controller/parser/templates/containerd_v2_no_GPU.toml.gtpl b/aks-node-controller/parser/templates/containerd_v2_no_GPU.toml.gtpl index 64438ad1e41..b684f31aecc 100644 --- a/aks-node-controller/parser/templates/containerd_v2_no_GPU.toml.gtpl +++ b/aks-node-controller/parser/templates/containerd_v2_no_GPU.toml.gtpl @@ -15,6 +15,14 @@ root = "{{.KubeletConfig.GetContainerDataDir}}"{{- end}} enable_tar_index = false {{- end}} [plugins."io.containerd.cri.v1.images"] +{{- with .GetContainerdConfig }} + {{- if ne .ImagePullWithSyncFs nil }} + image_pull_with_sync_fs = {{ .GetImagePullWithSyncFs }} + {{- end }} + {{- if ne .ImagePullProgressTimeout nil }} + image_pull_progress_timeout = {{ printf "%q" .GetImagePullProgressTimeout }} + {{- end }} +{{- end }} {{- if .GetEnableArtifactStreaming }} snapshotter = "overlaybd" disable_snapshot_annotations = false diff --git a/aks-node-controller/pkg/gen/aksnodeconfig/v1/containerd_config.pb.go b/aks-node-controller/pkg/gen/aksnodeconfig/v1/containerd_config.pb.go index 54d3a35de59..d81e9f245d6 100644 --- a/aks-node-controller/pkg/gen/aksnodeconfig/v1/containerd_config.pb.go +++ b/aks-node-controller/pkg/gen/aksnodeconfig/v1/containerd_config.pb.go @@ -31,6 +31,12 @@ type ContainerdConfig struct { ContainerdVersion string `protobuf:"bytes,2,opt,name=containerd_version,json=containerdVersion,proto3" json:"containerd_version,omitempty"` // The URL for downloading the containerd package. ContainerdPackageUrl string `protobuf:"bytes,3,opt,name=containerd_package_url,json=containerdPackageUrl,proto3" json:"containerd_package_url,omitempty"` + // Force filesystem synchronization during image unpacking. Absence preserves + // the existing setting. Enabling this may select local pulls on containerd 2.x. + ImagePullWithSyncFs *bool `protobuf:"varint,4,opt,name=image_pull_with_sync_fs,json=imagePullWithSyncFs,proto3,oneof" json:"image_pull_with_sync_fs,omitempty"` + // Maximum time without image download progress, in Go duration format. + // Zero disables the timeout. Absence preserves the existing setting. + ImagePullProgressTimeout *string `protobuf:"bytes,5,opt,name=image_pull_progress_timeout,json=imagePullProgressTimeout,proto3,oneof" json:"image_pull_progress_timeout,omitempty"` } func (x *ContainerdConfig) Reset() { @@ -84,13 +90,27 @@ func (x *ContainerdConfig) GetContainerdPackageUrl() string { return "" } +func (x *ContainerdConfig) GetImagePullWithSyncFs() bool { + if x != nil && x.ImagePullWithSyncFs != nil { + return *x.ImagePullWithSyncFs + } + return false +} + +func (x *ContainerdConfig) GetImagePullProgressTimeout() string { + if x != nil && x.ImagePullProgressTimeout != nil { + return *x.ImagePullProgressTimeout + } + return "" +} + var File_aksnodeconfig_v1_containerd_config_proto protoreflect.FileDescriptor var file_aksnodeconfig_v1_containerd_config_proto_rawDesc = []byte{ 0x0a, 0x28, 0x61, 0x6b, 0x73, 0x6e, 0x6f, 0x64, 0x65, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x76, 0x31, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x10, 0x61, 0x6b, 0x73, 0x6e, - 0x6f, 0x64, 0x65, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x22, 0xb8, 0x01, 0x0a, + 0x6f, 0x64, 0x65, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x76, 0x31, 0x22, 0xf3, 0x02, 0x0a, 0x10, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x3f, 0x0a, 0x1c, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x5f, 0x64, 0x6f, 0x77, 0x6e, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x75, 0x72, 0x6c, 0x5f, 0x62, 0x61, 0x73, @@ -102,13 +122,25 @@ var file_aksnodeconfig_v1_containerd_config_proto_rawDesc = []byte{ 0x6e, 0x12, 0x34, 0x0a, 0x16, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x5f, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x14, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x64, 0x50, 0x61, 0x63, - 0x6b, 0x61, 0x67, 0x65, 0x55, 0x72, 0x6c, 0x42, 0x5a, 0x5a, 0x58, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x2f, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x62, 0x61, 0x6b, 0x65, 0x72, 0x2f, 0x61, 0x6b, 0x73, 0x2d, 0x6e, 0x6f, 0x64, 0x65, 0x2d, - 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x67, - 0x65, 0x6e, 0x2f, 0x61, 0x6b, 0x73, 0x6e, 0x6f, 0x64, 0x65, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x2f, 0x76, 0x31, 0x3b, 0x61, 0x6b, 0x73, 0x6e, 0x6f, 0x64, 0x65, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x6b, 0x61, 0x67, 0x65, 0x55, 0x72, 0x6c, 0x12, 0x39, 0x0a, 0x17, 0x69, 0x6d, 0x61, 0x67, 0x65, + 0x5f, 0x70, 0x75, 0x6c, 0x6c, 0x5f, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x5f, + 0x66, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x13, 0x69, 0x6d, 0x61, 0x67, + 0x65, 0x50, 0x75, 0x6c, 0x6c, 0x57, 0x69, 0x74, 0x68, 0x53, 0x79, 0x6e, 0x63, 0x46, 0x73, 0x88, + 0x01, 0x01, 0x12, 0x42, 0x0a, 0x1b, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x70, 0x75, 0x6c, 0x6c, + 0x5f, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, + 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x18, 0x69, 0x6d, 0x61, 0x67, 0x65, + 0x50, 0x75, 0x6c, 0x6c, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x54, 0x69, 0x6d, 0x65, + 0x6f, 0x75, 0x74, 0x88, 0x01, 0x01, 0x42, 0x1a, 0x0a, 0x18, 0x5f, 0x69, 0x6d, 0x61, 0x67, 0x65, + 0x5f, 0x70, 0x75, 0x6c, 0x6c, 0x5f, 0x77, 0x69, 0x74, 0x68, 0x5f, 0x73, 0x79, 0x6e, 0x63, 0x5f, + 0x66, 0x73, 0x42, 0x1e, 0x0a, 0x1c, 0x5f, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x5f, 0x70, 0x75, 0x6c, + 0x6c, 0x5f, 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x6f, + 0x75, 0x74, 0x42, 0x5a, 0x5a, 0x58, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x41, 0x7a, 0x75, 0x72, 0x65, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x62, 0x61, 0x6b, 0x65, + 0x72, 0x2f, 0x61, 0x6b, 0x73, 0x2d, 0x6e, 0x6f, 0x64, 0x65, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x70, 0x6b, 0x67, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x61, 0x6b, + 0x73, 0x6e, 0x6f, 0x64, 0x65, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2f, 0x76, 0x31, 0x3b, 0x61, + 0x6b, 0x73, 0x6e, 0x6f, 0x64, 0x65, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x76, 0x31, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -140,6 +172,7 @@ func file_aksnodeconfig_v1_containerd_config_proto_init() { if File_aksnodeconfig_v1_containerd_config_proto != nil { return } + file_aksnodeconfig_v1_containerd_config_proto_msgTypes[0].OneofWrappers = []any{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/aks-node-controller/pkg/nodeconfigutils/containerd_test.go b/aks-node-controller/pkg/nodeconfigutils/containerd_test.go new file mode 100644 index 00000000000..c8eeabcf321 --- /dev/null +++ b/aks-node-controller/pkg/nodeconfigutils/containerd_test.go @@ -0,0 +1,60 @@ +package nodeconfigutils + +import ( + "testing" + + aksnodeconfigv1 "github.com/Azure/agentbaker/aks-node-controller/pkg/gen/aksnodeconfig/v1" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestContainerdImagePullValidationWithoutKubelet(t *testing.T) { + for _, timeout := range []string{"0", "0s", "30s", "5m", "1m30s"} { + cfg := &aksnodeconfigv1.Configuration{ + Version: "v1", + ContainerdConfig: &aksnodeconfigv1.ContainerdConfig{ + ImagePullWithSyncFs: proto.Bool(false), + ImagePullProgressTimeout: proto.String(timeout), + }, + } + require.NoError(t, validateAndNormalize(cfg)) + require.NoError(t, validateAndNormalize(cfg)) + require.Equal(t, timeout, cfg.ContainerdConfig.GetImagePullProgressTimeout()) + } + for _, timeout := range []string{"", "-1s", "invalid", "5", "999999999999999999999h", "5m\n"} { + cfg := &aksnodeconfigv1.Configuration{ + Version: "v1", + ContainerdConfig: &aksnodeconfigv1.ContainerdConfig{ImagePullProgressTimeout: proto.String(timeout)}, + } + require.ErrorContains(t, validateAndNormalize(cfg), "containerd_config.image_pull_progress_timeout") + } + require.NoError(t, validateAndNormalize(&aksnodeconfigv1.Configuration{Version: "v1"})) +} + +func TestContainerdImagePullPresenceRoundTrip(t *testing.T) { + for _, raw := range []string{ + `{"containerdConfig":{"imagePullWithSyncFs":false,"imagePullProgressTimeout":"0s"}}`, + `{"containerd_config":{"image_pull_with_sync_fs":true,"image_pull_progress_timeout":"30s"}}`, + `{"containerd_config":{}}`, + } { + cfg, err := UnmarshalConfigurationV1([]byte(raw)) + require.NoError(t, err) + encoded, err := MarshalConfigurationV1(cfg) + require.NoError(t, err) + restored, err := UnmarshalConfigurationV1(encoded) + require.NoError(t, err) + require.True(t, proto.Equal(cfg, restored)) + wire, err := proto.Marshal(cfg) + require.NoError(t, err) + decoded := &aksnodeconfigv1.Configuration{} + require.NoError(t, proto.Unmarshal(wire, decoded)) + require.True(t, proto.Equal(cfg, decoded)) + if cfg.ContainerdConfig.ImagePullWithSyncFs != nil { + require.Contains(t, string(encoded), `"image_pull_with_sync_fs"`) + require.NotNil(t, decoded.ContainerdConfig.ImagePullWithSyncFs) + require.NotNil(t, decoded.ContainerdConfig.ImagePullProgressTimeout) + } else { + require.NotContains(t, string(encoded), "image_pull_") + } + } +} diff --git a/aks-node-controller/pkg/nodeconfigutils/validation.go b/aks-node-controller/pkg/nodeconfigutils/validation.go index 81ae9941ce3..bda1208ec10 100644 --- a/aks-node-controller/pkg/nodeconfigutils/validation.go +++ b/aks-node-controller/pkg/nodeconfigutils/validation.go @@ -6,6 +6,7 @@ import ( "sort" "strconv" "strings" + "time" aksnodeconfigv1 "github.com/Azure/agentbaker/aks-node-controller/pkg/gen/aksnodeconfig/v1" "github.com/Masterminds/semver/v3" @@ -22,6 +23,9 @@ func validateAndNormalize(cfg *aksnodeconfigv1.Configuration) error { if err := validateCustomLinuxOSConfig(cfg.GetCustomLinuxOsConfig()); err != nil { return err } + if err := validateContainerdConfig(cfg.GetContainerdConfig()); err != nil { + return err + } kubelet := cfg.GetKubeletConfig() if kubelet == nil { @@ -57,6 +61,19 @@ func validateAndNormalize(cfg *aksnodeconfigv1.Configuration) error { return nil } +func validateContainerdConfig(cfg *aksnodeconfigv1.ContainerdConfig) error { + if cfg != nil && cfg.ImagePullProgressTimeout != nil { + duration, err := time.ParseDuration(cfg.GetImagePullProgressTimeout()) + if err != nil { + return fmt.Errorf("containerd_config.image_pull_progress_timeout: %w", err) + } + if duration < 0 { + return fmt.Errorf("containerd_config.image_pull_progress_timeout must not be negative") + } + } + return nil +} + func validateCustomLinuxOSConfig(osConfig *aksnodeconfigv1.CustomLinuxOsConfig) error { for _, field := range []struct { name, value string diff --git a/aks-node-controller/proto/aksnodeconfig/v1/containerd_config.proto b/aks-node-controller/proto/aksnodeconfig/v1/containerd_config.proto index 7a528c8f726..5eeccb015ca 100644 --- a/aks-node-controller/proto/aksnodeconfig/v1/containerd_config.proto +++ b/aks-node-controller/proto/aksnodeconfig/v1/containerd_config.proto @@ -12,4 +12,12 @@ message ContainerdConfig { // The URL for downloading the containerd package. string containerd_package_url = 3; + + // Force filesystem synchronization during image unpacking. Absence preserves + // the existing setting. Enabling this may select local pulls on containerd 2.x. + optional bool image_pull_with_sync_fs = 4; + + // Maximum time without image download progress, in Go duration format. + // Zero disables the timeout. Absence preserves the existing setting. + optional string image_pull_progress_timeout = 5; } From 698b46209d3f3418b24ec6686eee90ae6e3e80e8 Mon Sep 17 00:00:00 2001 From: Sri Harsha Date: Fri, 23 Oct 2026 20:48:48 +0000 Subject: [PATCH 2/4] feat(aks-node-controller): support optional containerd image-pull settings --- aks-node-controller/parser/containerd_test.go | 102 ------------------ aks-node-controller/parser/helper_test.go | 58 ++++++++++ aks-node-controller/parser/parser_test.go | 31 ++++++ .../pkg/nodeconfigutils/containerd_test.go | 60 ----------- .../pkg/nodeconfigutils/utils_test.go | 28 +++++ .../pkg/nodeconfigutils/validation_test.go | 23 ++++ 6 files changed, 140 insertions(+), 162 deletions(-) delete mode 100644 aks-node-controller/parser/containerd_test.go delete mode 100644 aks-node-controller/pkg/nodeconfigutils/containerd_test.go diff --git a/aks-node-controller/parser/containerd_test.go b/aks-node-controller/parser/containerd_test.go deleted file mode 100644 index fe10804d92c..00000000000 --- a/aks-node-controller/parser/containerd_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package parser - -import ( - "context" - "encoding/base64" - "fmt" - "os" - "path/filepath" - "regexp" - "strings" - "testing" - - aksnodeconfigv1 "github.com/Azure/agentbaker/aks-node-controller/pkg/gen/aksnodeconfig/v1" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" -) - -func TestContainerdImagePullTemplates(t *testing.T) { - for _, version := range []string{"1.7.22", "2.2.4"} { - for _, noGPU := range []bool{false, true} { - for _, tc := range []struct { - name string - settings *aksnodeconfigv1.ContainerdConfig - }{ - {"absent", nil}, - {"empty", &aksnodeconfigv1.ContainerdConfig{}}, - {"false", &aksnodeconfigv1.ContainerdConfig{ImagePullWithSyncFs: proto.Bool(false)}}, - {"true", &aksnodeconfigv1.ContainerdConfig{ImagePullWithSyncFs: proto.Bool(true)}}, - {"zero timeout", &aksnodeconfigv1.ContainerdConfig{ImagePullProgressTimeout: proto.String("0s")}}, - {"both", &aksnodeconfigv1.ContainerdConfig{ImagePullWithSyncFs: proto.Bool(true), ImagePullProgressTimeout: proto.String("1m30s")}}, - } { - t.Run(fmt.Sprintf("%s/noGPU=%t/%s", version, noGPU, tc.name), func(t *testing.T) { - cfg := &aksnodeconfigv1.Configuration{ - KubernetesVersion: "1.34.0", - GpuConfig: &aksnodeconfigv1.GpuConfig{EnableNvidia: proto.Bool(true)}, - ContainerdConfig: tc.settings, - } - rendered, err := containerdConfigFromAKSNodeConfig(cfg, noGPU, version) - require.NoError(t, err) - plugin := "io.containerd.grpc.v1.cri" - if isContainerdV2(version) { - plugin = "io.containerd.cri.v1.images" - } - header := `[plugins."` + plugin + `"]` + "\n" - start := strings.Index(rendered, header) - require.NotEqual(t, -1, start) - section := rendered[start+len(header):] - if next := regexp.MustCompile(`(?m)^[ \t]*\[`).FindStringIndex(section); next != nil { - section = section[:next[0]] - } - if tc.settings != nil && tc.settings.ImagePullWithSyncFs != nil { - require.Contains(t, section, fmt.Sprintf(" image_pull_with_sync_fs = %t\n", tc.settings.GetImagePullWithSyncFs())) - require.Equal(t, 1, strings.Count(rendered, "image_pull_with_sync_fs")) - } else { - require.NotContains(t, rendered, "image_pull_with_sync_fs") - } - if tc.settings != nil && tc.settings.ImagePullProgressTimeout != nil { - require.Contains(t, section, fmt.Sprintf(" image_pull_progress_timeout = %q\n", tc.settings.GetImagePullProgressTimeout())) - require.Equal(t, 1, strings.Count(rendered, "image_pull_progress_timeout")) - } else { - require.NotContains(t, rendered, "image_pull_progress_timeout") - } - if tc.settings == nil || (tc.settings.ImagePullWithSyncFs == nil && tc.settings.ImagePullProgressTimeout == nil) { - cfg.ContainerdConfig = nil - baseline, err := containerdConfigFromAKSNodeConfig(cfg, noGPU, version) - require.NoError(t, err) - require.Equal(t, baseline, rendered) - } - }) - } - } - } -} - -func TestBuildCSECmdContainerdImagePullSettings(t *testing.T) { - for _, version := range []string{"1.7.22", "2.2.4"} { - t.Run(version, func(t *testing.T) { - dir := t.TempDir() - require.NoError(t, os.WriteFile(filepath.Join(dir, "containerd"), []byte(fmt.Sprintf(`#!/bin/sh -[ "$1" = "--version" ] || exit 1 -echo "containerd containerd.io %s test" -`, version)), 0755)) - t.Setenv("PATH", dir) - cfg := &aksnodeconfigv1.Configuration{ContainerdConfig: &aksnodeconfigv1.ContainerdConfig{ - ImagePullWithSyncFs: proto.Bool(false), - ImagePullProgressTimeout: proto.String("0s"), - }} - cmd, err := BuildCSECmd(context.Background(), cfg, nil) - require.NoError(t, err) - env := environToMap(cmd.Env) - for _, key := range []string{"CONTAINERD_CONFIG_CONTENT", "CONTAINERD_CONFIG_NO_GPU_CONTENT"} { - content, err := base64.StdEncoding.DecodeString(env[key]) - require.NoError(t, err) - require.Contains(t, string(content), "image_pull_with_sync_fs = false") - require.Contains(t, string(content), `image_pull_progress_timeout = "0s"`) - } - require.NotContains(t, env, "ANC_CONFIGURE_CONTAINERD_BINARY") - require.NotContains(t, env, "CONTAINERD_IMAGE_PULL_WITH_SYNC_FS") - require.NotContains(t, env, "CONTAINERD_IMAGE_PULL_PROGRESS_TIMEOUT") - }) - } -} diff --git a/aks-node-controller/parser/helper_test.go b/aks-node-controller/parser/helper_test.go index d01a86134a5..59483f57316 100644 --- a/aks-node-controller/parser/helper_test.go +++ b/aks-node-controller/parser/helper_test.go @@ -21,6 +21,7 @@ import ( _ "embed" "encoding/base64" "encoding/json" + "fmt" "math" "reflect" "regexp" @@ -546,6 +547,63 @@ func Test_createSortedKeyValueInt32Pairs(t *testing.T) { } } +func TestContainerdImagePullTemplates(t *testing.T) { + for _, version := range []string{"1.7.22", "2.2.4"} { + for _, noGPU := range []bool{false, true} { + for _, tc := range []struct { + name string + settings *aksnodeconfigv1.ContainerdConfig + }{ + {"absent", nil}, + {"empty", &aksnodeconfigv1.ContainerdConfig{}}, + {"false", &aksnodeconfigv1.ContainerdConfig{ImagePullWithSyncFs: proto.Bool(false)}}, + {"true", &aksnodeconfigv1.ContainerdConfig{ImagePullWithSyncFs: proto.Bool(true)}}, + {"zero timeout", &aksnodeconfigv1.ContainerdConfig{ImagePullProgressTimeout: proto.String("0s")}}, + {"both", &aksnodeconfigv1.ContainerdConfig{ImagePullWithSyncFs: proto.Bool(true), ImagePullProgressTimeout: proto.String("1m30s")}}, + } { + t.Run(fmt.Sprintf("%s/noGPU=%t/%s", version, noGPU, tc.name), func(t *testing.T) { + cfg := &aksnodeconfigv1.Configuration{ + KubernetesVersion: "1.34.0", + GpuConfig: &aksnodeconfigv1.GpuConfig{EnableNvidia: proto.Bool(true)}, + ContainerdConfig: tc.settings, + } + rendered, err := containerdConfigFromAKSNodeConfig(cfg, noGPU, version) + require.NoError(t, err) + plugin := "io.containerd.grpc.v1.cri" + if isContainerdV2(version) { + plugin = "io.containerd.cri.v1.images" + } + header := `[plugins."` + plugin + `"]` + "\n" + start := strings.Index(rendered, header) + require.NotEqual(t, -1, start) + section := rendered[start+len(header):] + if next := regexp.MustCompile(`(?m)^[ \t]*\[`).FindStringIndex(section); next != nil { + section = section[:next[0]] + } + if tc.settings != nil && tc.settings.ImagePullWithSyncFs != nil { + require.Contains(t, section, fmt.Sprintf(" image_pull_with_sync_fs = %t\n", tc.settings.GetImagePullWithSyncFs())) + require.Equal(t, 1, strings.Count(rendered, "image_pull_with_sync_fs")) + } else { + require.NotContains(t, rendered, "image_pull_with_sync_fs") + } + if tc.settings != nil && tc.settings.ImagePullProgressTimeout != nil { + require.Contains(t, section, fmt.Sprintf(" image_pull_progress_timeout = %q\n", tc.settings.GetImagePullProgressTimeout())) + require.Equal(t, 1, strings.Count(rendered, "image_pull_progress_timeout")) + } else { + require.NotContains(t, rendered, "image_pull_progress_timeout") + } + if tc.settings == nil || (tc.settings.ImagePullWithSyncFs == nil && tc.settings.ImagePullProgressTimeout == nil) { + cfg.ContainerdConfig = nil + baseline, err := containerdConfigFromAKSNodeConfig(cfg, noGPU, version) + require.NoError(t, err) + require.Equal(t, baseline, rendered) + } + }) + } + } + } +} + func Test_getContainerdConfig(t *testing.T) { type args struct { aksnodeconfig *aksnodeconfigv1.Configuration diff --git a/aks-node-controller/parser/parser_test.go b/aks-node-controller/parser/parser_test.go index fc6109fc6ab..da20f558e6d 100644 --- a/aks-node-controller/parser/parser_test.go +++ b/aks-node-controller/parser/parser_test.go @@ -7,6 +7,7 @@ import ( "log" "os" "os/exec" + "path/filepath" "strings" "testing" @@ -17,8 +18,38 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" ) +func TestBuildCSECmdContainerdImagePullSettings(t *testing.T) { + for _, version := range []string{"1.7.22", "2.2.4"} { + t.Run(version, func(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.WriteFile(filepath.Join(dir, "containerd"), []byte(fmt.Sprintf(`#!/bin/sh +[ "$1" = "--version" ] || exit 1 +echo "containerd containerd.io %s test" +`, version)), 0755)) + t.Setenv("PATH", dir) + cfg := &aksnodeconfigv1.Configuration{ContainerdConfig: &aksnodeconfigv1.ContainerdConfig{ + ImagePullWithSyncFs: proto.Bool(false), + ImagePullProgressTimeout: proto.String("0s"), + }} + cmd, err := BuildCSECmd(context.Background(), cfg, nil) + require.NoError(t, err) + env := environToMap(cmd.Env) + for _, key := range []string{"CONTAINERD_CONFIG_CONTENT", "CONTAINERD_CONFIG_NO_GPU_CONTENT"} { + content, err := base64.StdEncoding.DecodeString(env[key]) + require.NoError(t, err) + require.Contains(t, string(content), "image_pull_with_sync_fs = false") + require.Contains(t, string(content), `image_pull_progress_timeout = "0s"`) + } + require.NotContains(t, env, "ANC_CONFIGURE_CONTAINERD_BINARY") + require.NotContains(t, env, "CONTAINERD_IMAGE_PULL_WITH_SYNC_FS") + require.NotContains(t, env, "CONTAINERD_IMAGE_PULL_PROGRESS_TIMEOUT") + }) + } +} + func TestBuildCSECmd(t *testing.T) { tests := []struct { name string diff --git a/aks-node-controller/pkg/nodeconfigutils/containerd_test.go b/aks-node-controller/pkg/nodeconfigutils/containerd_test.go deleted file mode 100644 index c8eeabcf321..00000000000 --- a/aks-node-controller/pkg/nodeconfigutils/containerd_test.go +++ /dev/null @@ -1,60 +0,0 @@ -package nodeconfigutils - -import ( - "testing" - - aksnodeconfigv1 "github.com/Azure/agentbaker/aks-node-controller/pkg/gen/aksnodeconfig/v1" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" -) - -func TestContainerdImagePullValidationWithoutKubelet(t *testing.T) { - for _, timeout := range []string{"0", "0s", "30s", "5m", "1m30s"} { - cfg := &aksnodeconfigv1.Configuration{ - Version: "v1", - ContainerdConfig: &aksnodeconfigv1.ContainerdConfig{ - ImagePullWithSyncFs: proto.Bool(false), - ImagePullProgressTimeout: proto.String(timeout), - }, - } - require.NoError(t, validateAndNormalize(cfg)) - require.NoError(t, validateAndNormalize(cfg)) - require.Equal(t, timeout, cfg.ContainerdConfig.GetImagePullProgressTimeout()) - } - for _, timeout := range []string{"", "-1s", "invalid", "5", "999999999999999999999h", "5m\n"} { - cfg := &aksnodeconfigv1.Configuration{ - Version: "v1", - ContainerdConfig: &aksnodeconfigv1.ContainerdConfig{ImagePullProgressTimeout: proto.String(timeout)}, - } - require.ErrorContains(t, validateAndNormalize(cfg), "containerd_config.image_pull_progress_timeout") - } - require.NoError(t, validateAndNormalize(&aksnodeconfigv1.Configuration{Version: "v1"})) -} - -func TestContainerdImagePullPresenceRoundTrip(t *testing.T) { - for _, raw := range []string{ - `{"containerdConfig":{"imagePullWithSyncFs":false,"imagePullProgressTimeout":"0s"}}`, - `{"containerd_config":{"image_pull_with_sync_fs":true,"image_pull_progress_timeout":"30s"}}`, - `{"containerd_config":{}}`, - } { - cfg, err := UnmarshalConfigurationV1([]byte(raw)) - require.NoError(t, err) - encoded, err := MarshalConfigurationV1(cfg) - require.NoError(t, err) - restored, err := UnmarshalConfigurationV1(encoded) - require.NoError(t, err) - require.True(t, proto.Equal(cfg, restored)) - wire, err := proto.Marshal(cfg) - require.NoError(t, err) - decoded := &aksnodeconfigv1.Configuration{} - require.NoError(t, proto.Unmarshal(wire, decoded)) - require.True(t, proto.Equal(cfg, decoded)) - if cfg.ContainerdConfig.ImagePullWithSyncFs != nil { - require.Contains(t, string(encoded), `"image_pull_with_sync_fs"`) - require.NotNil(t, decoded.ContainerdConfig.ImagePullWithSyncFs) - require.NotNil(t, decoded.ContainerdConfig.ImagePullProgressTimeout) - } else { - require.NotContains(t, string(encoded), "image_pull_") - } - } -} diff --git a/aks-node-controller/pkg/nodeconfigutils/utils_test.go b/aks-node-controller/pkg/nodeconfigutils/utils_test.go index a296a82df8c..5bea3f89bdc 100644 --- a/aks-node-controller/pkg/nodeconfigutils/utils_test.go +++ b/aks-node-controller/pkg/nodeconfigutils/utils_test.go @@ -16,6 +16,34 @@ import ( "google.golang.org/protobuf/proto" ) +func TestContainerdImagePullPresenceRoundTrip(t *testing.T) { + for _, raw := range []string{ + `{"containerdConfig":{"imagePullWithSyncFs":false,"imagePullProgressTimeout":"0s"}}`, + `{"containerd_config":{"image_pull_with_sync_fs":true,"image_pull_progress_timeout":"30s"}}`, + `{"containerd_config":{}}`, + } { + cfg, err := UnmarshalConfigurationV1([]byte(raw)) + require.NoError(t, err) + encoded, err := MarshalConfigurationV1(cfg) + require.NoError(t, err) + restored, err := UnmarshalConfigurationV1(encoded) + require.NoError(t, err) + require.True(t, proto.Equal(cfg, restored)) + wire, err := proto.Marshal(cfg) + require.NoError(t, err) + decoded := &aksnodeconfigv1.Configuration{} + require.NoError(t, proto.Unmarshal(wire, decoded)) + require.True(t, proto.Equal(cfg, decoded)) + if cfg.ContainerdConfig.ImagePullWithSyncFs != nil { + require.Contains(t, string(encoded), `"image_pull_with_sync_fs"`) + require.NotNil(t, decoded.ContainerdConfig.ImagePullWithSyncFs) + require.NotNil(t, decoded.ContainerdConfig.ImagePullProgressTimeout) + } else { + require.NotContains(t, string(encoded), "image_pull_") + } + } +} + func TestUnmarshalConfigurationV1(t *testing.T) { tests := []struct { name string diff --git a/aks-node-controller/pkg/nodeconfigutils/validation_test.go b/aks-node-controller/pkg/nodeconfigutils/validation_test.go index 30a02be0f11..0b46de02af3 100644 --- a/aks-node-controller/pkg/nodeconfigutils/validation_test.go +++ b/aks-node-controller/pkg/nodeconfigutils/validation_test.go @@ -8,6 +8,29 @@ import ( "google.golang.org/protobuf/proto" ) +func TestContainerdImagePullValidationWithoutKubelet(t *testing.T) { + for _, timeout := range []string{"0", "0s", "30s", "5m", "1m30s"} { + cfg := &aksnodeconfigv1.Configuration{ + Version: "v1", + ContainerdConfig: &aksnodeconfigv1.ContainerdConfig{ + ImagePullWithSyncFs: proto.Bool(false), + ImagePullProgressTimeout: proto.String(timeout), + }, + } + require.NoError(t, validateAndNormalize(cfg)) + require.NoError(t, validateAndNormalize(cfg)) + require.Equal(t, timeout, cfg.ContainerdConfig.GetImagePullProgressTimeout()) + } + for _, timeout := range []string{"", "-1s", "invalid", "5", "999999999999999999999h", "5m\n"} { + cfg := &aksnodeconfigv1.Configuration{ + Version: "v1", + ContainerdConfig: &aksnodeconfigv1.ContainerdConfig{ImagePullProgressTimeout: proto.String(timeout)}, + } + require.ErrorContains(t, validateAndNormalize(cfg), "containerd_config.image_pull_progress_timeout") + } + require.NoError(t, validateAndNormalize(&aksnodeconfigv1.Configuration{Version: "v1"})) +} + func TestValidateAndNormalize(t *testing.T) { for _, version := range []string{"1.10.0", "1.11.0", "1.20.0", "1.24.0", "1.25.0", "1.34.0", "1.34.0-rc.1"} { t.Run(version, func(t *testing.T) { From 34324e806292c77d32b1d0118ee6d54d74c19697 Mon Sep 17 00:00:00 2001 From: Sri Harsha Date: Fri, 23 Oct 2026 21:03:49 +0000 Subject: [PATCH 3/4] feat(aks-node-controller): support optional containerd image-pull settings --- aks-node-controller/parser/helper_test.go | 77 ++++++++++++----------- 1 file changed, 41 insertions(+), 36 deletions(-) diff --git a/aks-node-controller/parser/helper_test.go b/aks-node-controller/parser/helper_test.go index 59483f57316..97ab2f8e957 100644 --- a/aks-node-controller/parser/helper_test.go +++ b/aks-node-controller/parser/helper_test.go @@ -562,48 +562,53 @@ func TestContainerdImagePullTemplates(t *testing.T) { {"both", &aksnodeconfigv1.ContainerdConfig{ImagePullWithSyncFs: proto.Bool(true), ImagePullProgressTimeout: proto.String("1m30s")}}, } { t.Run(fmt.Sprintf("%s/noGPU=%t/%s", version, noGPU, tc.name), func(t *testing.T) { - cfg := &aksnodeconfigv1.Configuration{ - KubernetesVersion: "1.34.0", - GpuConfig: &aksnodeconfigv1.GpuConfig{EnableNvidia: proto.Bool(true)}, - ContainerdConfig: tc.settings, - } - rendered, err := containerdConfigFromAKSNodeConfig(cfg, noGPU, version) - require.NoError(t, err) - plugin := "io.containerd.grpc.v1.cri" - if isContainerdV2(version) { - plugin = "io.containerd.cri.v1.images" - } - header := `[plugins."` + plugin + `"]` + "\n" - start := strings.Index(rendered, header) - require.NotEqual(t, -1, start) - section := rendered[start+len(header):] - if next := regexp.MustCompile(`(?m)^[ \t]*\[`).FindStringIndex(section); next != nil { - section = section[:next[0]] - } - if tc.settings != nil && tc.settings.ImagePullWithSyncFs != nil { - require.Contains(t, section, fmt.Sprintf(" image_pull_with_sync_fs = %t\n", tc.settings.GetImagePullWithSyncFs())) - require.Equal(t, 1, strings.Count(rendered, "image_pull_with_sync_fs")) - } else { - require.NotContains(t, rendered, "image_pull_with_sync_fs") - } - if tc.settings != nil && tc.settings.ImagePullProgressTimeout != nil { - require.Contains(t, section, fmt.Sprintf(" image_pull_progress_timeout = %q\n", tc.settings.GetImagePullProgressTimeout())) - require.Equal(t, 1, strings.Count(rendered, "image_pull_progress_timeout")) - } else { - require.NotContains(t, rendered, "image_pull_progress_timeout") - } - if tc.settings == nil || (tc.settings.ImagePullWithSyncFs == nil && tc.settings.ImagePullProgressTimeout == nil) { - cfg.ContainerdConfig = nil - baseline, err := containerdConfigFromAKSNodeConfig(cfg, noGPU, version) - require.NoError(t, err) - require.Equal(t, baseline, rendered) - } + assertContainerdImagePullTemplate(t, tc.settings, noGPU, version) }) } } } } +func assertContainerdImagePullTemplate(t *testing.T, settings *aksnodeconfigv1.ContainerdConfig, noGPU bool, version string) { + t.Helper() + cfg := &aksnodeconfigv1.Configuration{ + KubernetesVersion: "1.34.0", + GpuConfig: &aksnodeconfigv1.GpuConfig{EnableNvidia: proto.Bool(true)}, + ContainerdConfig: settings, + } + rendered, err := containerdConfigFromAKSNodeConfig(cfg, noGPU, version) + require.NoError(t, err) + plugin := "io.containerd.grpc.v1.cri" + if isContainerdV2(version) { + plugin = "io.containerd.cri.v1.images" + } + header := `[plugins."` + plugin + `"]` + "\n" + start := strings.Index(rendered, header) + require.NotEqual(t, -1, start) + section := rendered[start+len(header):] + if next := regexp.MustCompile(`(?m)^[ \t]*\[`).FindStringIndex(section); next != nil { + section = section[:next[0]] + } + if settings != nil && settings.ImagePullWithSyncFs != nil { + require.Contains(t, section, fmt.Sprintf(" image_pull_with_sync_fs = %t\n", settings.GetImagePullWithSyncFs())) + require.Equal(t, 1, strings.Count(rendered, "image_pull_with_sync_fs")) + } else { + require.NotContains(t, rendered, "image_pull_with_sync_fs") + } + if settings != nil && settings.ImagePullProgressTimeout != nil { + require.Contains(t, section, fmt.Sprintf(" image_pull_progress_timeout = %q\n", settings.GetImagePullProgressTimeout())) + require.Equal(t, 1, strings.Count(rendered, "image_pull_progress_timeout")) + } else { + require.NotContains(t, rendered, "image_pull_progress_timeout") + } + if settings == nil || (settings.ImagePullWithSyncFs == nil && settings.ImagePullProgressTimeout == nil) { + cfg.ContainerdConfig = nil + baseline, err := containerdConfigFromAKSNodeConfig(cfg, noGPU, version) + require.NoError(t, err) + require.Equal(t, baseline, rendered) + } +} + func Test_getContainerdConfig(t *testing.T) { type args struct { aksnodeconfig *aksnodeconfigv1.Configuration From 7efa96cb88e44bfa62405aa8c2dbf7c9d96e548f Mon Sep 17 00:00:00 2001 From: Sri Harsha Date: Mon, 26 Oct 2026 23:48:59 +0000 Subject: [PATCH 4/4] Use explicit quotes for containerd image pull timeout --- aks-node-controller/parser/templates/containerd.toml.gtpl | 2 +- .../parser/templates/containerd_no_GPU.toml.gtpl | 2 +- aks-node-controller/parser/templates/containerd_v2.toml.gtpl | 2 +- .../parser/templates/containerd_v2_no_GPU.toml.gtpl | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/aks-node-controller/parser/templates/containerd.toml.gtpl b/aks-node-controller/parser/templates/containerd.toml.gtpl index 3dd0be790eb..475aeea83c5 100644 --- a/aks-node-controller/parser/templates/containerd.toml.gtpl +++ b/aks-node-controller/parser/templates/containerd.toml.gtpl @@ -20,7 +20,7 @@ root = "{{.KubeletConfig.GetContainerDataDir}}"{{- end}} image_pull_with_sync_fs = {{ .GetImagePullWithSyncFs }} {{- end }} {{- if ne .ImagePullProgressTimeout nil }} - image_pull_progress_timeout = {{ printf "%q" .GetImagePullProgressTimeout }} + image_pull_progress_timeout = "{{ .GetImagePullProgressTimeout }}" {{- end }} {{- end }} sandbox_image = "{{ .KubeBinaryConfig.GetPodInfraContainerImageUrl }}" diff --git a/aks-node-controller/parser/templates/containerd_no_GPU.toml.gtpl b/aks-node-controller/parser/templates/containerd_no_GPU.toml.gtpl index 71c9435ecdb..d4bea918fec 100644 --- a/aks-node-controller/parser/templates/containerd_no_GPU.toml.gtpl +++ b/aks-node-controller/parser/templates/containerd_no_GPU.toml.gtpl @@ -20,7 +20,7 @@ root = "{{.KubeletConfig.GetContainerDataDir}}"{{- end}} image_pull_with_sync_fs = {{ .GetImagePullWithSyncFs }} {{- end }} {{- if ne .ImagePullProgressTimeout nil }} - image_pull_progress_timeout = {{ printf "%q" .GetImagePullProgressTimeout }} + image_pull_progress_timeout = "{{ .GetImagePullProgressTimeout }}" {{- end }} {{- end }} sandbox_image = "{{ .KubeBinaryConfig.GetPodInfraContainerImageUrl }}" diff --git a/aks-node-controller/parser/templates/containerd_v2.toml.gtpl b/aks-node-controller/parser/templates/containerd_v2.toml.gtpl index 195b58fa1d6..52f2ca39521 100644 --- a/aks-node-controller/parser/templates/containerd_v2.toml.gtpl +++ b/aks-node-controller/parser/templates/containerd_v2.toml.gtpl @@ -20,7 +20,7 @@ root = "{{.KubeletConfig.GetContainerDataDir}}"{{- end}} image_pull_with_sync_fs = {{ .GetImagePullWithSyncFs }} {{- end }} {{- if ne .ImagePullProgressTimeout nil }} - image_pull_progress_timeout = {{ printf "%q" .GetImagePullProgressTimeout }} + image_pull_progress_timeout = "{{ .GetImagePullProgressTimeout }}" {{- end }} {{- end }} {{- if .GetEnableArtifactStreaming }} diff --git a/aks-node-controller/parser/templates/containerd_v2_no_GPU.toml.gtpl b/aks-node-controller/parser/templates/containerd_v2_no_GPU.toml.gtpl index b684f31aecc..3213d6e8e27 100644 --- a/aks-node-controller/parser/templates/containerd_v2_no_GPU.toml.gtpl +++ b/aks-node-controller/parser/templates/containerd_v2_no_GPU.toml.gtpl @@ -20,7 +20,7 @@ root = "{{.KubeletConfig.GetContainerDataDir}}"{{- end}} image_pull_with_sync_fs = {{ .GetImagePullWithSyncFs }} {{- end }} {{- if ne .ImagePullProgressTimeout nil }} - image_pull_progress_timeout = {{ printf "%q" .GetImagePullProgressTimeout }} + image_pull_progress_timeout = "{{ .GetImagePullProgressTimeout }}" {{- end }} {{- end }} {{- if .GetEnableArtifactStreaming }}