diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index ca36f2299f8..c4892686716 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -897,6 +897,13 @@ const ( // alpha: v1.29 // LoadBalancerIPMode enables the IPMode field in the LoadBalancerIngress status of a Service LoadBalancerIPMode featuregate.Feature = "LoadBalancerIPMode" + + // owner: @haircommander + // kep: http://kep.k8s.io/4210 + // alpha: v1.29 + // ImageMaximumGCAge enables the Kubelet configuration field of the same name, allowing an admin + // to specify the age after which an image will be garbage collected. + ImageMaximumGCAge featuregate.Feature = "ImageMaximumGCAge" ) func init() { @@ -1140,6 +1147,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS LoadBalancerIPMode: {Default: false, PreRelease: featuregate.Alpha}, + ImageMaximumGCAge: {Default: false, PreRelease: featuregate.Alpha}, + // inherited features from generic apiserver, relisted here to get a conflict if it is changed // unintentionally on either side: diff --git a/pkg/generated/openapi/zz_generated.openapi.go b/pkg/generated/openapi/zz_generated.openapi.go index b8e37305b6e..33809d1496b 100644 --- a/pkg/generated/openapi/zz_generated.openapi.go +++ b/pkg/generated/openapi/zz_generated.openapi.go @@ -55592,6 +55592,12 @@ func schema_k8sio_kubelet_config_v1beta1_KubeletConfiguration(ref common.Referen Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Duration"), }, }, + "imageMaximumGCAge": { + SchemaProps: spec.SchemaProps{ + Description: "imageMaximumGCAge is the maximum age an image can be unused before it is garbage collected. The default of this field is \"0s\", which disables this field--meaning images won't be garbage collected based on being unused for too long. Default: \"0s\" (disabled)", + Ref: ref("k8s.io/apimachinery/pkg/apis/meta/v1.Duration"), + }, + }, "imageGCHighThresholdPercent": { SchemaProps: spec.SchemaProps{ Description: "imageGCHighThresholdPercent is the percent of disk usage after which image garbage collection is always run. The percent is calculated by dividing this field value by 100, so this field must be between 0 and 100, inclusive. When specified, the value must be greater than imageGCLowThresholdPercent. Default: 85", diff --git a/pkg/kubelet/apis/config/fuzzer/fuzzer.go b/pkg/kubelet/apis/config/fuzzer/fuzzer.go index 79748b4b93f..dfa988c0d04 100644 --- a/pkg/kubelet/apis/config/fuzzer/fuzzer.go +++ b/pkg/kubelet/apis/config/fuzzer/fuzzer.go @@ -61,6 +61,7 @@ func Funcs(codecs runtimeserializer.CodecFactory) []interface{} { obj.HealthzPort = 10248 obj.HTTPCheckFrequency = metav1.Duration{Duration: 20 * time.Second} obj.ImageMinimumGCAge = metav1.Duration{Duration: 2 * time.Minute} + obj.ImageMaximumGCAge = metav1.Duration{} obj.ImageGCHighThresholdPercent = 85 obj.ImageGCLowThresholdPercent = 80 obj.KernelMemcgNotification = false diff --git a/pkg/kubelet/apis/config/helpers_test.go b/pkg/kubelet/apis/config/helpers_test.go index 286602099f4..31f406b85f2 100644 --- a/pkg/kubelet/apis/config/helpers_test.go +++ b/pkg/kubelet/apis/config/helpers_test.go @@ -230,6 +230,7 @@ var ( "ImageGCHighThresholdPercent", "ImageGCLowThresholdPercent", "ImageMinimumGCAge.Duration", + "ImageMaximumGCAge.Duration", "KernelMemcgNotification", "KubeAPIBurst", "KubeAPIQPS", diff --git a/pkg/kubelet/apis/config/scheme/testdata/KubeletConfiguration/after/v1beta1.yaml b/pkg/kubelet/apis/config/scheme/testdata/KubeletConfiguration/after/v1beta1.yaml index ab43f865f3a..def3f0dc844 100644 --- a/pkg/kubelet/apis/config/scheme/testdata/KubeletConfiguration/after/v1beta1.yaml +++ b/pkg/kubelet/apis/config/scheme/testdata/KubeletConfiguration/after/v1beta1.yaml @@ -43,6 +43,7 @@ healthzPort: 10248 httpCheckFrequency: 20s imageGCHighThresholdPercent: 85 imageGCLowThresholdPercent: 80 +imageMaximumGCAge: 0s imageMinimumGCAge: 2m0s iptablesDropBit: 15 iptablesMasqueradeBit: 14 diff --git a/pkg/kubelet/apis/config/scheme/testdata/KubeletConfiguration/roundtrip/default/v1beta1.yaml b/pkg/kubelet/apis/config/scheme/testdata/KubeletConfiguration/roundtrip/default/v1beta1.yaml index 8b877c3ad4e..ca5cf18b983 100644 --- a/pkg/kubelet/apis/config/scheme/testdata/KubeletConfiguration/roundtrip/default/v1beta1.yaml +++ b/pkg/kubelet/apis/config/scheme/testdata/KubeletConfiguration/roundtrip/default/v1beta1.yaml @@ -43,6 +43,7 @@ healthzPort: 10248 httpCheckFrequency: 20s imageGCHighThresholdPercent: 85 imageGCLowThresholdPercent: 80 +imageMaximumGCAge: 0s imageMinimumGCAge: 2m0s iptablesDropBit: 15 iptablesMasqueradeBit: 14 diff --git a/pkg/kubelet/apis/config/types.go b/pkg/kubelet/apis/config/types.go index 1bff0cec0ca..a64724a58d1 100644 --- a/pkg/kubelet/apis/config/types.go +++ b/pkg/kubelet/apis/config/types.go @@ -192,9 +192,13 @@ type KubeletConfiguration struct { NodeStatusReportFrequency metav1.Duration // nodeLeaseDurationSeconds is the duration the Kubelet will set on its corresponding Lease. NodeLeaseDurationSeconds int32 - // imageMinimumGCAge is the minimum age for an unused image before it is + // ImageMinimumGCAge is the minimum age for an unused image before it is // garbage collected. ImageMinimumGCAge metav1.Duration + // ImageMaximumGCAge is the maximum age an image can be unused before it is garbage collected. + // The default of this field is "0s", which disables this field--meaning images won't be garbage + // collected based on being unused for too long. + ImageMaximumGCAge metav1.Duration // imageGCHighThresholdPercent is the percent of disk usage after which // image garbage collection is always run. The percent is calculated as // this field value out of 100. diff --git a/pkg/kubelet/apis/config/v1beta1/defaults_test.go b/pkg/kubelet/apis/config/v1beta1/defaults_test.go index b387f50b625..55a6068e44d 100644 --- a/pkg/kubelet/apis/config/v1beta1/defaults_test.go +++ b/pkg/kubelet/apis/config/v1beta1/defaults_test.go @@ -77,6 +77,7 @@ func TestSetDefaultsKubeletConfiguration(t *testing.T) { NodeStatusReportFrequency: metav1.Duration{Duration: 5 * time.Minute}, NodeLeaseDurationSeconds: 40, ImageMinimumGCAge: metav1.Duration{Duration: 2 * time.Minute}, + ImageMaximumGCAge: metav1.Duration{}, ImageGCHighThresholdPercent: utilpointer.Int32(85), ImageGCLowThresholdPercent: utilpointer.Int32(80), ContainerRuntimeEndpoint: "unix:///run/containerd/containerd.sock", diff --git a/pkg/kubelet/apis/config/v1beta1/zz_generated.conversion.go b/pkg/kubelet/apis/config/v1beta1/zz_generated.conversion.go index 5376913670c..3704be3e1a1 100644 --- a/pkg/kubelet/apis/config/v1beta1/zz_generated.conversion.go +++ b/pkg/kubelet/apis/config/v1beta1/zz_generated.conversion.go @@ -392,6 +392,7 @@ func autoConvert_v1beta1_KubeletConfiguration_To_config_KubeletConfiguration(in out.NodeStatusReportFrequency = in.NodeStatusReportFrequency out.NodeLeaseDurationSeconds = in.NodeLeaseDurationSeconds out.ImageMinimumGCAge = in.ImageMinimumGCAge + out.ImageMaximumGCAge = in.ImageMaximumGCAge if err := v1.Convert_Pointer_int32_To_int32(&in.ImageGCHighThresholdPercent, &out.ImageGCHighThresholdPercent, s); err != nil { return err } @@ -579,6 +580,7 @@ func autoConvert_config_KubeletConfiguration_To_v1beta1_KubeletConfiguration(in out.NodeStatusReportFrequency = in.NodeStatusReportFrequency out.NodeLeaseDurationSeconds = in.NodeLeaseDurationSeconds out.ImageMinimumGCAge = in.ImageMinimumGCAge + out.ImageMaximumGCAge = in.ImageMaximumGCAge if err := v1.Convert_int32_To_Pointer_int32(&in.ImageGCHighThresholdPercent, &out.ImageGCHighThresholdPercent, s); err != nil { return err } diff --git a/pkg/kubelet/apis/config/zz_generated.deepcopy.go b/pkg/kubelet/apis/config/zz_generated.deepcopy.go index a4af47e4a83..e2c0cc1dd1e 100644 --- a/pkg/kubelet/apis/config/zz_generated.deepcopy.go +++ b/pkg/kubelet/apis/config/zz_generated.deepcopy.go @@ -202,6 +202,7 @@ func (in *KubeletConfiguration) DeepCopyInto(out *KubeletConfiguration) { out.NodeStatusUpdateFrequency = in.NodeStatusUpdateFrequency out.NodeStatusReportFrequency = in.NodeStatusReportFrequency out.ImageMinimumGCAge = in.ImageMinimumGCAge + out.ImageMaximumGCAge = in.ImageMaximumGCAge out.VolumeStatsAggPeriod = in.VolumeStatsAggPeriod if in.CPUManagerPolicyOptions != nil { in, out := &in.CPUManagerPolicyOptions, &out.CPUManagerPolicyOptions diff --git a/pkg/kubelet/images/image_gc_manager.go b/pkg/kubelet/images/image_gc_manager.go index e33a826c3cd..12f1056883a 100644 --- a/pkg/kubelet/images/image_gc_manager.go +++ b/pkg/kubelet/images/image_gc_manager.go @@ -78,6 +78,11 @@ type ImageGCPolicy struct { // Minimum age at which an image can be garbage collected. MinAge time.Duration + + // Maximum age after which an image can be garbage collected, regardless of disk usage. + // Currently gated by MaximumImageGCAge feature gate and Kubelet configuration. + // If 0, the feature is disabled. + MaxAge time.Duration } type realImageGCManager struct { diff --git a/pkg/kubelet/kubelet.go b/pkg/kubelet/kubelet.go index e6040d1b256..c26eb0a9d13 100644 --- a/pkg/kubelet/kubelet.go +++ b/pkg/kubelet/kubelet.go @@ -424,6 +424,12 @@ func NewMainKubelet(kubeCfg *kubeletconfiginternal.KubeletConfiguration, LowThresholdPercent: int(kubeCfg.ImageGCLowThresholdPercent), } + if utilfeature.DefaultFeatureGate.Enabled(features.ImageMaximumGCAge) { + imageGCPolicy.MaxAge = kubeCfg.ImageMaximumGCAge.Duration + } else if kubeCfg.ImageMaximumGCAge.Duration != 0 { + klog.InfoS("ImageMaximumGCAge flag enabled, but corresponding feature gate is not enabled. Ignoring flag.") + } + enforceNodeAllocatable := kubeCfg.EnforceNodeAllocatable if experimentalNodeAllocatableIgnoreEvictionThreshold { // Do not provide kubeCfg.EnforceNodeAllocatable to eviction threshold parsing if we are not enforcing Evictions diff --git a/staging/src/k8s.io/kubelet/config/v1beta1/types.go b/staging/src/k8s.io/kubelet/config/v1beta1/types.go index b1ad1353fca..fd1439e9ebf 100644 --- a/staging/src/k8s.io/kubelet/config/v1beta1/types.go +++ b/staging/src/k8s.io/kubelet/config/v1beta1/types.go @@ -290,6 +290,12 @@ type KubeletConfiguration struct { // Default: "2m" // +optional ImageMinimumGCAge metav1.Duration `json:"imageMinimumGCAge,omitempty"` + // imageMaximumGCAge is the maximum age an image can be unused before it is garbage collected. + // The default of this field is "0s", which disables this field--meaning images won't be garbage + // collected based on being unused for too long. + // Default: "0s" (disabled) + // +optional + ImageMaximumGCAge metav1.Duration `json:"imageMaximumGCAge,omitempty"` // imageGCHighThresholdPercent is the percent of disk usage after which // image garbage collection is always run. The percent is calculated by // dividing this field value by 100, so this field must be between 0 and diff --git a/staging/src/k8s.io/kubelet/config/v1beta1/zz_generated.deepcopy.go b/staging/src/k8s.io/kubelet/config/v1beta1/zz_generated.deepcopy.go index 85e564e3ea9..ff653a9923c 100644 --- a/staging/src/k8s.io/kubelet/config/v1beta1/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/kubelet/config/v1beta1/zz_generated.deepcopy.go @@ -237,6 +237,7 @@ func (in *KubeletConfiguration) DeepCopyInto(out *KubeletConfiguration) { out.NodeStatusUpdateFrequency = in.NodeStatusUpdateFrequency out.NodeStatusReportFrequency = in.NodeStatusReportFrequency out.ImageMinimumGCAge = in.ImageMinimumGCAge + out.ImageMaximumGCAge = in.ImageMaximumGCAge if in.ImageGCHighThresholdPercent != nil { in, out := &in.ImageGCHighThresholdPercent, &out.ImageGCHighThresholdPercent *out = new(int32)