diff --git a/cmd/kubelet/app/server.go b/cmd/kubelet/app/server.go index c39be3c0fe1..f069f6b08b9 100644 --- a/cmd/kubelet/app/server.go +++ b/cmd/kubelet/app/server.go @@ -741,10 +741,8 @@ func run(ctx context.Context, s *options.KubeletServer, kubeDeps *kubelet.Depend } // Get cgroup driver setting from CRI - if utilfeature.DefaultFeatureGate.Enabled(features.KubeletCgroupDriverFromCRI) { - if err := getCgroupDriverFromCRI(ctx, s, kubeDeps); err != nil { - return err - } + if err := getCgroupDriverFromCRI(ctx, s, kubeDeps); err != nil { + return err } var cgroupRoots []string @@ -1397,7 +1395,9 @@ func getCgroupDriverFromCRI(ctx context.Context, s *options.KubeletServer, kubeD continue } // CRI implementation doesn't support RuntimeConfig, fallback - logger.Info("CRI implementation should be updated to support RuntimeConfig when KubeletCgroupDriverFromCRI feature gate has been enabled. Falling back to using cgroupDriver from kubelet config.") + legacyregistry.MustRegister(kubeletmetrics.CRILosingSupport) + kubeletmetrics.CRILosingSupport.WithLabelValues("1.36.0").Inc() + logger.Info("CRI implementation should be updated to support RuntimeConfig. Falling back to using cgroupDriver from kubelet config.") return nil } } diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index 9db20e589b8..549b59cea55 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -1311,6 +1311,7 @@ var defaultVersionedKubernetesFeatureGates = map[featuregate.Feature]featuregate KubeletCgroupDriverFromCRI: { {Version: version.MustParse("1.28"), Default: false, PreRelease: featuregate.Alpha}, {Version: version.MustParse("1.31"), Default: true, PreRelease: featuregate.Beta}, + {Version: version.MustParse("1.34"), Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.37 }, KubeletCrashLoopBackOffMax: { diff --git a/pkg/kubelet/metrics/metrics.go b/pkg/kubelet/metrics/metrics.go index 684c912f420..05d496681d9 100644 --- a/pkg/kubelet/metrics/metrics.go +++ b/pkg/kubelet/metrics/metrics.go @@ -74,6 +74,7 @@ const ( RestartedPodTotalKey = "restarted_pods_total" ImagePullDurationKey = "image_pull_duration_seconds" CgroupVersionKey = "cgroup_version" + CRILosingSupportKey = "cri_losing_support" // Metrics keys of remote runtime operations RuntimeOperationsKey = "runtime_operations_total" @@ -1018,6 +1019,16 @@ var ( }, ) + CRILosingSupport = metrics.NewGaugeVec( + &metrics.GaugeOpts{ + Subsystem: KubeletSubsystem, + Name: CRILosingSupportKey, + Help: "the Kubernetes version that the currently running CRI implementation will lose support on if not upgraded.", + StabilityLevel: metrics.ALPHA, + }, + []string{"version"}, + ) + // DRAOperationsDuration tracks the duration of the DRA PrepareResources and UnprepareResources requests. DRAOperationsDuration = metrics.NewHistogramVec( &metrics.HistogramOpts{ diff --git a/test/compatibility_lifecycle/reference/versioned_feature_list.yaml b/test/compatibility_lifecycle/reference/versioned_feature_list.yaml index 47dd2b777f1..2c07075bce7 100644 --- a/test/compatibility_lifecycle/reference/versioned_feature_list.yaml +++ b/test/compatibility_lifecycle/reference/versioned_feature_list.yaml @@ -731,6 +731,10 @@ lockToDefault: false preRelease: Beta version: "1.31" + - default: true + lockToDefault: true + preRelease: GA + version: "1.34" - name: KubeletCrashLoopBackOffMax versionedSpecs: - default: false diff --git a/test/e2e_node/cgroup_driver_from_cri_test.go b/test/e2e_node/cgroup_driver_from_cri_test.go new file mode 100644 index 00000000000..d40143ca988 --- /dev/null +++ b/test/e2e_node/cgroup_driver_from_cri_test.go @@ -0,0 +1,83 @@ +//go:build linux +// +build linux + +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2enode + +import ( + "context" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/onsi/ginkgo/v2" + "github.com/onsi/gomega" + + kubeletmetrics "k8s.io/kubernetes/pkg/kubelet/metrics" + "k8s.io/kubernetes/test/e2e/feature" + "k8s.io/kubernetes/test/e2e/framework" + e2emetrics "k8s.io/kubernetes/test/e2e/framework/metrics" + "k8s.io/kubernetes/test/e2e_node/criproxy" + admissionapi "k8s.io/pod-security-admission/api" +) + +var _ = SIGDescribe("Cgroup Driver From CRI", feature.CriProxy, framework.WithSerial(), func() { + f := framework.NewDefaultFramework("cgroup-driver-from-cri") + f.NamespacePodSecurityLevel = admissionapi.LevelPrivileged + ginkgo.Context("", func() { + ginkgo.BeforeEach(func() { + if err := resetCRIProxyInjector(e2eCriProxy); err != nil { + ginkgo.Skip("Skip the test since the CRI Proxy is undefined.") + } + ginkgo.DeferCleanup(func() error { + return resetCRIProxyInjector(e2eCriProxy) + }) + }) + + ginkgo.It("should only report a metric if CRI is outdated", func(ctx context.Context) { + expectedErr := status.Error(codes.Unimplemented, "unimplemented") + err := addCRIProxyInjector(e2eCriProxy, func(apiName string) error { + if apiName == criproxy.RuntimeConfig { + return expectedErr + } + return nil + }) + framework.ExpectNoError(err) + restartKubelet(context.Background(), true) + time.Sleep(time.Second * 1) + + m, err := e2emetrics.GrabKubeletMetricsWithoutProxy(context.Background(), nodeNameOrIP()+":10255", "/metrics") + framework.ExpectNoError(err) + samples := m[kubeletmetrics.KubeletSubsystem+"_"+kubeletmetrics.CRILosingSupportKey] + + gomega.Expect(samples).NotTo(gomega.BeEmpty()) + gomega.Expect(samples[0].Metric["version"]).To(gomega.BeEquivalentTo("1.36.0")) + }) + ginkgo.It("should not emit metric if CRI is new enough", func() { + restartKubelet(context.Background(), true) + time.Sleep(time.Second * 1) + + m, err := e2emetrics.GrabKubeletMetricsWithoutProxy(context.Background(), nodeNameOrIP()+":10255", "/metrics") + framework.ExpectNoError(err) + samples := m[kubeletmetrics.KubeletSubsystem+"_"+kubeletmetrics.CRILosingSupportKey] + + gomega.Expect(samples).To(gomega.BeEmpty()) + }) + }) +})