From b3415bfdfe2958e7015f0c880f1154df95aada63 Mon Sep 17 00:00:00 2001 From: carlory Date: Mon, 27 Oct 2025 17:46:29 +0800 Subject: [PATCH] kubeadm: added container runtime version check to preflight Co-authored-by: Lubomir I. Ivanov Signed-off-by: carlory --- .../app/cmd/phases/upgrade/apply/preflight.go | 2 +- .../app/cmd/phases/upgrade/node/preflight.go | 2 +- cmd/kubeadm/app/preflight/checks.go | 99 +++++++++++---- cmd/kubeadm/app/preflight/checks_test.go | 84 +++++++++++++ .../{runtime_fake_test.go => fake_impl.go} | 69 ++++++++--- cmd/kubeadm/app/util/runtime/impl.go | 8 +- cmd/kubeadm/app/util/runtime/runtime.go | 25 +++- cmd/kubeadm/app/util/runtime/runtime_test.go | 117 +++++++++++++----- 8 files changed, 328 insertions(+), 78 deletions(-) rename cmd/kubeadm/app/util/runtime/{runtime_fake_test.go => fake_impl.go} (52%) diff --git a/cmd/kubeadm/app/cmd/phases/upgrade/apply/preflight.go b/cmd/kubeadm/app/cmd/phases/upgrade/apply/preflight.go index 55a387927bd..06d45cea242 100644 --- a/cmd/kubeadm/app/cmd/phases/upgrade/apply/preflight.go +++ b/cmd/kubeadm/app/cmd/phases/upgrade/apply/preflight.go @@ -70,7 +70,7 @@ func runPreflight(c workflow.RunData) error { if err := preflight.RunRootCheckOnly(ignorePreflightErrors); err != nil { return err } - if err := preflight.RunUpgradeChecks(utilsexec.New(), ignorePreflightErrors); err != nil { + if err := preflight.RunUpgradeChecks(utilsexec.New(), initCfg, ignorePreflightErrors); err != nil { return err } diff --git a/cmd/kubeadm/app/cmd/phases/upgrade/node/preflight.go b/cmd/kubeadm/app/cmd/phases/upgrade/node/preflight.go index 43c35d3ecb2..a9985884e52 100644 --- a/cmd/kubeadm/app/cmd/phases/upgrade/node/preflight.go +++ b/cmd/kubeadm/app/cmd/phases/upgrade/node/preflight.go @@ -53,7 +53,7 @@ func runPreflight(c workflow.RunData) error { if err := preflight.RunRootCheckOnly(data.IgnorePreflightErrors()); err != nil { return err } - if err := preflight.RunUpgradeChecks(utilsexec.New(), data.IgnorePreflightErrors()); err != nil { + if err := preflight.RunUpgradeChecks(utilsexec.New(), data.InitCfg(), data.IgnorePreflightErrors()); err != nil { return err } diff --git a/cmd/kubeadm/app/preflight/checks.go b/cmd/kubeadm/app/preflight/checks.go index f6cc2c47421..162ed260b4c 100644 --- a/cmd/kubeadm/app/preflight/checks.go +++ b/cmd/kubeadm/app/preflight/checks.go @@ -73,7 +73,10 @@ type Checker interface { // ContainerRuntimeCheck verifies the container runtime. type ContainerRuntimeCheck struct { - runtime utilruntime.ContainerRuntime + criSocket string + + // stubbed out for testing + impl utilruntime.Impl } // Name returns label for RuntimeCheck. @@ -84,13 +87,62 @@ func (ContainerRuntimeCheck) Name() string { // Check validates the container runtime func (crc ContainerRuntimeCheck) Check() (warnings, errorList []error) { klog.V(1).Infoln("validating the container runtime") - defer crc.runtime.Close() - if err := crc.runtime.IsRunning(); err != nil { + containerRuntime := utilruntime.NewContainerRuntime(crc.criSocket) + if crc.impl != nil { + containerRuntime.SetImpl(crc.impl) + } + if err := containerRuntime.Connect(); err != nil { + return nil, []error{errors.Wrap(err, "could not connect to the container runtime")} + } + defer containerRuntime.Close() + + if err := containerRuntime.IsRunning(); err != nil { errorList = append(errorList, err) } return warnings, errorList } +// ContainerRuntimeVersionCheck verifies the version compatibility between installed container runtime and kubelet. +type ContainerRuntimeVersionCheck struct { + criSocket string + + // stubbed out for testing + impl utilruntime.Impl +} + +// Name returns label for RuntimeCheck. +func (ContainerRuntimeVersionCheck) Name() string { + return "ContainerRuntimeVersion" +} + +// Check validates the container runtime version compatibility with kubelet. +func (crvc ContainerRuntimeVersionCheck) Check() (warnings, errorList []error) { + klog.V(1).Infoln("validating the container runtime version compatibility") + containerRuntime := utilruntime.NewContainerRuntime(crvc.criSocket) + if crvc.impl != nil { + containerRuntime.SetImpl(crvc.impl) + } + if err := containerRuntime.Connect(); err != nil { + return nil, []error{errors.Wrap(err, "could not connect to the container runtime")} + } + defer containerRuntime.Close() + + ok, err := containerRuntime.IsRuntimeConfigImplemented() + if err != nil { + return nil, []error{errors.Wrap(err, "could not check if the runtime config is available")} + } + if !ok { + // TODO: return an error once the kubelet version is 1.36 or higher. + // https://github.com/kubernetes/kubeadm/issues/3229 + err := errors.New("You must update your container runtime to a version that supports the CRI method RuntimeConfig. " + + "Falling back to using cgroupDriver from kubelet config will be removed in 1.36. " + + "For more information, see https://git.k8s.io/enhancements/keps/sig-node/4033-group-driver-detection-over-cri") + warnings = append(warnings, err) + } + + return warnings, errorList +} + // ServiceCheck verifies that the given service is enabled and active. If we do not // detect a supported init system however, all checks are skipped and a warning is // returned. @@ -807,11 +859,14 @@ func getEtcdVersionResponse(client *http.Client, url string, target interface{}) // ImagePullCheck will pull container images used by kubeadm type ImagePullCheck struct { - runtime utilruntime.ContainerRuntime + criSocket string imageList []string sandboxImage string imagePullPolicy v1.PullPolicy imagePullSerial bool + + // stubbed out for testing + impl utilruntime.Impl } // Name returns the label for ImagePullCheck @@ -821,6 +876,15 @@ func (ImagePullCheck) Name() string { // Check pulls images required by kubeadm. This is a mutating check func (ipc ImagePullCheck) Check() (warnings, errorList []error) { + containerRuntime := utilruntime.NewContainerRuntime(ipc.criSocket) + if ipc.impl != nil { + containerRuntime.SetImpl(ipc.impl) + } + if err := containerRuntime.Connect(); err != nil { + return nil, []error{errors.Wrap(err, "could not connect to the container runtime")} + } + defer containerRuntime.Close() + // Handle unsupported image pull policy and policy Never. policy := ipc.imagePullPolicy switch policy { @@ -835,7 +899,7 @@ func (ipc ImagePullCheck) Check() (warnings, errorList []error) { } // Handle CRI sandbox image warnings. - criSandboxImage, err := ipc.runtime.SandboxImage() + criSandboxImage, err := containerRuntime.SandboxImage() if err != nil { klog.V(4).Infof("failed to detect the sandbox image for local container runtime, %v", err) } else if criSandboxImage != ipc.sandboxImage { @@ -845,7 +909,7 @@ func (ipc ImagePullCheck) Check() (warnings, errorList []error) { // Perform parallel pulls. if !ipc.imagePullSerial { - if err := ipc.runtime.PullImagesInParallel(ipc.imageList, policy == v1.PullIfNotPresent); err != nil { + if err := containerRuntime.PullImagesInParallel(ipc.imageList, policy == v1.PullIfNotPresent); err != nil { errorList = append(errorList, err) } return warnings, errorList @@ -855,14 +919,14 @@ func (ipc ImagePullCheck) Check() (warnings, errorList []error) { for _, image := range ipc.imageList { switch policy { case v1.PullIfNotPresent: - if ipc.runtime.ImageExists(image) { + if containerRuntime.ImageExists(image) { klog.V(1).Infof("image exists: %s", image) continue } fallthrough // Proceed with pulling the image if it does not exist case v1.PullAlways: klog.V(1).Infof("pulling: %s", image) - if err := ipc.runtime.PullImage(image); err != nil { + if err := containerRuntime.PullImage(image); err != nil { errorList = append(errorList, errors.WithMessagef(err, "failed to pull image %s", image)) } } @@ -1056,12 +1120,8 @@ func RunJoinNodeChecks(execer utilsexec.Interface, cfg *kubeadmapi.JoinConfigura // addCommonChecks is a helper function to duplicate checks that are common between both the // kubeadm init and join commands func addCommonChecks(execer utilsexec.Interface, k8sVersion string, nodeReg *kubeadmapi.NodeRegistrationOptions, checks []Checker) []Checker { - containerRuntime := utilruntime.NewContainerRuntime(nodeReg.CRISocket) - if err := containerRuntime.Connect(); err != nil { - klog.Warningf("[preflight] WARNING: Couldn't create the interface used for talking to the container runtime: %v\n", err) - } else { - checks = append(checks, ContainerRuntimeCheck{runtime: containerRuntime}) - } + checks = append(checks, ContainerRuntimeCheck{criSocket: nodeReg.CRISocket}) + checks = append(checks, ContainerRuntimeVersionCheck{criSocket: nodeReg.CRISocket}) // non-windows checks checks = addSwapCheck(checks) @@ -1085,9 +1145,10 @@ func RunRootCheckOnly(ignorePreflightErrors sets.Set[string]) error { } // RunUpgradeChecks initializes checks slice of structs and call RunChecks -func RunUpgradeChecks(execer utilsexec.Interface, ignorePreflightErrors sets.Set[string]) error { +func RunUpgradeChecks(execer utilsexec.Interface, cfg *kubeadmapi.InitConfiguration, ignorePreflightErrors sets.Set[string]) error { checks := []Checker{ SystemVerificationCheck{isUpgrade: true, exec: execer}, + ContainerRuntimeVersionCheck{criSocket: cfg.NodeRegistration.CRISocket}, } return RunChecks(checks, os.Stderr, ignorePreflightErrors) @@ -1095,12 +1156,6 @@ func RunUpgradeChecks(execer utilsexec.Interface, ignorePreflightErrors sets.Set // RunPullImagesCheck will pull images kubeadm needs if they are not found on the system func RunPullImagesCheck(execer utilsexec.Interface, cfg *kubeadmapi.InitConfiguration, ignorePreflightErrors sets.Set[string]) error { - containerRuntime := utilruntime.NewContainerRuntime(cfg.NodeRegistration.CRISocket) - if err := containerRuntime.Connect(); err != nil { - return handleError(os.Stderr, err.Error()) - } - defer containerRuntime.Close() - serialPull := true if cfg.NodeRegistration.ImagePullSerial != nil { serialPull = *cfg.NodeRegistration.ImagePullSerial @@ -1108,7 +1163,7 @@ func RunPullImagesCheck(execer utilsexec.Interface, cfg *kubeadmapi.InitConfigur checks := []Checker{ ImagePullCheck{ - runtime: containerRuntime, + criSocket: cfg.NodeRegistration.CRISocket, imageList: images.GetControlPlaneImages(&cfg.ClusterConfiguration), sandboxImage: images.GetPauseImage(&cfg.ClusterConfiguration), imagePullPolicy: cfg.NodeRegistration.ImagePullPolicy, diff --git a/cmd/kubeadm/app/preflight/checks_test.go b/cmd/kubeadm/app/preflight/checks_test.go index 1720677797f..1f61802617b 100644 --- a/cmd/kubeadm/app/preflight/checks_test.go +++ b/cmd/kubeadm/app/preflight/checks_test.go @@ -28,6 +28,8 @@ import ( "github.com/google/go-cmp/cmp" "github.com/lithammer/dedent" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/version" @@ -39,6 +41,7 @@ import ( kubeadmapiv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta4" configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config" "k8s.io/kubernetes/cmd/kubeadm/app/util/errors" + utilruntime "k8s.io/kubernetes/cmd/kubeadm/app/util/runtime" ) var ( @@ -1021,3 +1024,84 @@ func TestJoinIPCheck(t *testing.T) { }) } } + +func TestContainerRuntimeCheck(t *testing.T) { + tests := []struct { + name string + prepare func(*utilruntime.FakeImpl) + expectErrors int + expectWarnings int + }{ + { + name: "ok", + }, + { + name: "container runtime is not running", + prepare: func(mock *utilruntime.FakeImpl) { + mock.StatusReturns(nil, errors.New("not running")) + }, + expectErrors: 1, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + mock := &utilruntime.FakeImpl{} + if test.prepare != nil { + test.prepare(mock) + } + + warnings, errors := ContainerRuntimeCheck{impl: mock}.Check() + if len(warnings) != test.expectWarnings { + t.Errorf("expected %d warning(s) but got %d: %q", test.expectWarnings, len(warnings), warnings) + } + if len(errors) != test.expectErrors { + t.Errorf("expected %d error(s) but got %d: %q", test.expectErrors, len(errors), errors) + } + }) + } +} + +func TestContainerRuntimeVersionCheck(t *testing.T) { + tests := []struct { + name string + prepare func(*utilruntime.FakeImpl) + expectErrors int + expectWarnings int + }{ + { + name: "ok", + }, + { + name: "runtime config not implemented", + prepare: func(mock *utilruntime.FakeImpl) { + mock.RuntimeConfigReturns(nil, status.New(codes.Unimplemented, "not implemented").Err()) + }, + expectWarnings: 1, + }, + { + name: "call RuntimeConfig fails", + prepare: func(mock *utilruntime.FakeImpl) { + mock.RuntimeConfigReturns(nil, status.New(codes.DeadlineExceeded, "deadline exceeded").Err()) + }, + expectErrors: 1, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + mock := &utilruntime.FakeImpl{} + if test.prepare != nil { + test.prepare(mock) + } + + warnings, errors := ContainerRuntimeVersionCheck{impl: mock}.Check() + if len(warnings) != test.expectWarnings { + t.Errorf("expected %d warning(s) but got %d: %q", test.expectWarnings, len(warnings), warnings) + } + if len(errors) != test.expectErrors { + t.Errorf("expected %d error(s) but got %d: %q", test.expectErrors, len(errors), errors) + } + }) + } +} diff --git a/cmd/kubeadm/app/util/runtime/runtime_fake_test.go b/cmd/kubeadm/app/util/runtime/fake_impl.go similarity index 52% rename from cmd/kubeadm/app/util/runtime/runtime_fake_test.go rename to cmd/kubeadm/app/util/runtime/fake_impl.go index e42e239e575..b3bde5a3510 100644 --- a/cmd/kubeadm/app/util/runtime/runtime_fake_test.go +++ b/cmd/kubeadm/app/util/runtime/fake_impl.go @@ -24,7 +24,12 @@ import ( v1 "k8s.io/cri-api/pkg/apis/runtime/v1" ) -type fakeImpl struct { +// FakeImpl is a fake implementation of the impl interface. +type FakeImpl struct { + runtimeConfigReturns struct { + res *v1.RuntimeConfigResponse + err error + } imageStatusReturns struct { res *v1.ImageStatusResponse err error @@ -57,95 +62,125 @@ type fakeImpl struct { } } -func (fake *fakeImpl) ImageStatus(context.Context, cri.ImageManagerService, *v1.ImageSpec, bool) (*v1.ImageStatusResponse, error) { +// ImageStatus returns the status of the image. +func (fake *FakeImpl) ImageStatus(context.Context, cri.ImageManagerService, *v1.ImageSpec, bool) (*v1.ImageStatusResponse, error) { fakeReturns := fake.imageStatusReturns return fakeReturns.res, fakeReturns.err } -func (fake *fakeImpl) ImageStatusReturns(res *v1.ImageStatusResponse, err error) { +// ImageStatusReturns sets the return values for the ImageStatus method. +func (fake *FakeImpl) ImageStatusReturns(res *v1.ImageStatusResponse, err error) { fake.imageStatusReturns = struct { res *v1.ImageStatusResponse err error }{res, err} } -func (fake *fakeImpl) ListPodSandbox(context.Context, cri.RuntimeService, *v1.PodSandboxFilter) ([]*v1.PodSandbox, error) { +// ListPodSandbox returns the list of pod sandboxes. +func (fake *FakeImpl) ListPodSandbox(context.Context, cri.RuntimeService, *v1.PodSandboxFilter) ([]*v1.PodSandbox, error) { fakeReturns := fake.listPodSandboxReturns return fakeReturns.res, fakeReturns.err } -func (fake *fakeImpl) ListPodSandboxReturns(res []*v1.PodSandbox, err error) { +// ListPodSandboxReturns sets the return values for the ListPodSandbox method. +func (fake *FakeImpl) ListPodSandboxReturns(res []*v1.PodSandbox, err error) { fake.listPodSandboxReturns = struct { res []*v1.PodSandbox err error }{res, err} } -func (fake *fakeImpl) NewRemoteImageService(string, time.Duration) (cri.ImageManagerService, error) { +// NewRemoteImageService returns the new remote image service. +func (fake *FakeImpl) NewRemoteImageService(string, time.Duration) (cri.ImageManagerService, error) { fakeReturns := fake.newRemoteImageServiceReturns return fakeReturns.res, fakeReturns.err } -func (fake *fakeImpl) NewRemoteImageServiceReturns(res cri.ImageManagerService, err error) { +// NewRemoteImageServiceReturns sets the return values for the NewRemoteImageService method. +func (fake *FakeImpl) NewRemoteImageServiceReturns(res cri.ImageManagerService, err error) { fake.newRemoteImageServiceReturns = struct { res cri.ImageManagerService err error }{res, err} } -func (fake *fakeImpl) NewRemoteRuntimeService(string, time.Duration) (cri.RuntimeService, error) { +// NewRemoteRuntimeService returns the new remote runtime service. +func (fake *FakeImpl) NewRemoteRuntimeService(string, time.Duration) (cri.RuntimeService, error) { fakeReturns := fake.newRemoteRuntimeServiceReturns return fakeReturns.res, fakeReturns.err } -func (fake *fakeImpl) NewRemoteRuntimeServiceReturns(res cri.RuntimeService, err error) { +// NewRemoteRuntimeServiceReturns sets the return values for the NewRemoteRuntimeService method. +func (fake *FakeImpl) NewRemoteRuntimeServiceReturns(res cri.RuntimeService, err error) { fake.newRemoteRuntimeServiceReturns = struct { res cri.RuntimeService err error }{res, err} } -func (fake *fakeImpl) PullImage(context.Context, cri.ImageManagerService, *v1.ImageSpec, *v1.AuthConfig, *v1.PodSandboxConfig) (string, error) { +// PullImage returns the pull image. +func (fake *FakeImpl) PullImage(context.Context, cri.ImageManagerService, *v1.ImageSpec, *v1.AuthConfig, *v1.PodSandboxConfig) (string, error) { fakeReturns := fake.pullImageReturns return fakeReturns.res, fakeReturns.err } -func (fake *fakeImpl) PullImageReturns(res string, err error) { +// PullImageReturns sets the return values for the PullImage method. +func (fake *FakeImpl) PullImageReturns(res string, err error) { fake.pullImageReturns = struct { res string err error }{res, err} } -func (fake *fakeImpl) RemovePodSandbox(context.Context, cri.RuntimeService, string) error { +// RemovePodSandbox removes the pod sandbox. +func (fake *FakeImpl) RemovePodSandbox(context.Context, cri.RuntimeService, string) error { fakeReturns := fake.removePodSandboxReturns return fakeReturns.res } -func (fake *fakeImpl) RemovePodSandboxReturns(res error) { +// RemovePodSandboxReturns sets the return values for the RemovePodSandbox method. +func (fake *FakeImpl) RemovePodSandboxReturns(res error) { fake.removePodSandboxReturns = struct { res error }{res} } -func (fake *fakeImpl) Status(context.Context, cri.RuntimeService, bool) (*v1.StatusResponse, error) { +// RuntimeConfig returns the runtime config. +func (fake *FakeImpl) RuntimeConfig(context.Context, cri.RuntimeService) (*v1.RuntimeConfigResponse, error) { + fakeReturns := fake.runtimeConfigReturns + return fakeReturns.res, fakeReturns.err +} + +// RuntimeConfigReturns sets the return values for the RuntimeConfig method. +func (fake *FakeImpl) RuntimeConfigReturns(res *v1.RuntimeConfigResponse, err error) { + fake.runtimeConfigReturns = struct { + res *v1.RuntimeConfigResponse + err error + }{res, err} +} + +// Status returns the status of the runtime. +func (fake *FakeImpl) Status(context.Context, cri.RuntimeService, bool) (*v1.StatusResponse, error) { fakeReturns := fake.statusReturns return fakeReturns.res, fakeReturns.err } -func (fake *fakeImpl) StatusReturns(res *v1.StatusResponse, err error) { +// StatusReturns sets the return values for the Status method. +func (fake *FakeImpl) StatusReturns(res *v1.StatusResponse, err error) { fake.statusReturns = struct { res *v1.StatusResponse err error }{res, err} } -func (fake *fakeImpl) StopPodSandbox(context.Context, cri.RuntimeService, string) error { +// StopPodSandbox stops the pod sandbox. +func (fake *FakeImpl) StopPodSandbox(context.Context, cri.RuntimeService, string) error { fakeReturns := fake.stopPodSandboxReturns return fakeReturns.res } -func (fake *fakeImpl) StopPodSandboxReturns(res error) { +// StopPodSandboxReturns sets the return values for the StopPodSandbox method. +func (fake *FakeImpl) StopPodSandboxReturns(res error) { fake.stopPodSandboxReturns = struct { res error }{res} diff --git a/cmd/kubeadm/app/util/runtime/impl.go b/cmd/kubeadm/app/util/runtime/impl.go index b71f589d45c..e44f56b3824 100644 --- a/cmd/kubeadm/app/util/runtime/impl.go +++ b/cmd/kubeadm/app/util/runtime/impl.go @@ -27,9 +27,11 @@ import ( type defaultImpl struct{} -type impl interface { +// Impl is an interface for the container runtime implementation. +type Impl interface { NewRemoteRuntimeService(endpoint string, connectionTimeout time.Duration) (criapi.RuntimeService, error) NewRemoteImageService(endpoint string, connectionTimeout time.Duration) (criapi.ImageManagerService, error) + RuntimeConfig(ctx context.Context, runtimeService criapi.RuntimeService) (*runtimeapi.RuntimeConfigResponse, error) Status(ctx context.Context, runtimeService criapi.RuntimeService, verbose bool) (*runtimeapi.StatusResponse, error) ListPodSandbox(ctx context.Context, runtimeService criapi.RuntimeService, filter *runtimeapi.PodSandboxFilter) ([]*runtimeapi.PodSandbox, error) StopPodSandbox(ctx context.Context, runtimeService criapi.RuntimeService, sandboxID string) error @@ -46,6 +48,10 @@ func (*defaultImpl) NewRemoteImageService(endpoint string, connectionTimeout tim return criclient.NewRemoteImageService(endpoint, connectionTimeout, nil, nil) } +func (*defaultImpl) RuntimeConfig(ctx context.Context, runtimeService criapi.RuntimeService) (*runtimeapi.RuntimeConfigResponse, error) { + return runtimeService.RuntimeConfig(ctx) +} + func (*defaultImpl) Status(ctx context.Context, runtimeService criapi.RuntimeService, verbose bool) (*runtimeapi.StatusResponse, error) { return runtimeService.Status(ctx, verbose) } diff --git a/cmd/kubeadm/app/util/runtime/runtime.go b/cmd/kubeadm/app/util/runtime/runtime.go index 826330cda94..831d1478e8c 100644 --- a/cmd/kubeadm/app/util/runtime/runtime.go +++ b/cmd/kubeadm/app/util/runtime/runtime.go @@ -23,6 +23,9 @@ import ( "strings" "time" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + errorsutil "k8s.io/apimachinery/pkg/util/errors" criapi "k8s.io/cri-api/pkg/apis" runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1" @@ -43,7 +46,7 @@ var defaultKnownCRISockets = []string{ type ContainerRuntime interface { Connect() error Close() - SetImpl(impl) + SetImpl(Impl) IsRunning() error ListKubeContainers() ([]string, error) RemoveContainers(containers []string) error @@ -51,11 +54,12 @@ type ContainerRuntime interface { PullImagesInParallel(images []string, ifNotPresent bool) error ImageExists(image string) bool SandboxImage() (string, error) + IsRuntimeConfigImplemented() (bool, error) } // CRIRuntime is a struct that interfaces with the CRI type CRIRuntime struct { - impl impl + impl Impl criSocket string runtimeService criapi.RuntimeService imageService criapi.ImageManagerService @@ -73,7 +77,7 @@ func NewContainerRuntime(criSocket string) ContainerRuntime { } // SetImpl can be used to set the internal implementation for testing purposes. -func (runtime *CRIRuntime) SetImpl(impl impl) { +func (runtime *CRIRuntime) SetImpl(impl Impl) { runtime.impl = impl } @@ -314,3 +318,18 @@ func (runtime *CRIRuntime) SandboxImage() (string, error) { return c.SandboxImage, nil } + +// IsRuntimeConfigImplemented checks if the container runtime supports the RuntimeConfig gRPC method +func (runtime *CRIRuntime) IsRuntimeConfigImplemented() (bool, error) { + ctx, cancel := defaultContext() + defer cancel() + _, err := runtime.impl.RuntimeConfig(ctx, runtime.runtimeService) + if err != nil { + s, ok := status.FromError(err) + if !ok || s.Code() != codes.Unimplemented { + return false, errors.Wrap(err, "failed to call RuntimeConfig gRPC method") + } + return false, nil + } + return true, nil +} diff --git a/cmd/kubeadm/app/util/runtime/runtime_test.go b/cmd/kubeadm/app/util/runtime/runtime_test.go index b1048dc792d..7b6d60ebe8b 100644 --- a/cmd/kubeadm/app/util/runtime/runtime_test.go +++ b/cmd/kubeadm/app/util/runtime/runtime_test.go @@ -24,6 +24,8 @@ import ( "testing" "github.com/stretchr/testify/assert" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" v1 "k8s.io/cri-api/pkg/apis/runtime/v1" @@ -32,11 +34,12 @@ import ( ) var errTest = errors.New("test") +var errNotImplemented = status.New(codes.Unimplemented, "not implemented").Err() func TestNewContainerRuntime(t *testing.T) { for _, tc := range []struct { name string - prepare func(*fakeImpl) + prepare func(*FakeImpl) shouldError bool }{ { @@ -45,14 +48,14 @@ func TestNewContainerRuntime(t *testing.T) { }, { name: "invalid: new runtime service fails", - prepare: func(mock *fakeImpl) { + prepare: func(mock *FakeImpl) { mock.NewRemoteRuntimeServiceReturns(nil, errTest) }, shouldError: true, }, { name: "invalid: new image service fails", - prepare: func(mock *fakeImpl) { + prepare: func(mock *FakeImpl) { mock.NewRemoteImageServiceReturns(nil, errTest) }, shouldError: true, @@ -60,7 +63,7 @@ func TestNewContainerRuntime(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { containerRuntime := NewContainerRuntime("") - mock := &fakeImpl{} + mock := &FakeImpl{} if tc.prepare != nil { tc.prepare(mock) } @@ -76,7 +79,7 @@ func TestNewContainerRuntime(t *testing.T) { func TestIsRunning(t *testing.T) { for _, tc := range []struct { name string - prepare func(*fakeImpl) + prepare func(*FakeImpl) shouldError bool }{ { @@ -85,14 +88,14 @@ func TestIsRunning(t *testing.T) { }, { name: "invalid: runtime status fails", - prepare: func(mock *fakeImpl) { + prepare: func(mock *FakeImpl) { mock.StatusReturns(nil, errTest) }, shouldError: true, }, { name: "invalid: runtime condition status not 'true'", - prepare: func(mock *fakeImpl) { + prepare: func(mock *FakeImpl) { mock.StatusReturns(&v1.StatusResponse{Status: &v1.RuntimeStatus{ Conditions: []*v1.RuntimeCondition{ { @@ -107,7 +110,7 @@ func TestIsRunning(t *testing.T) { }, { name: "valid: runtime condition type does not match", - prepare: func(mock *fakeImpl) { + prepare: func(mock *FakeImpl) { mock.StatusReturns(&v1.StatusResponse{Status: &v1.RuntimeStatus{ Conditions: []*v1.RuntimeCondition{ { @@ -123,7 +126,7 @@ func TestIsRunning(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { containerRuntime := NewContainerRuntime("") - mock := &fakeImpl{} + mock := &FakeImpl{} if tc.prepare != nil { tc.prepare(mock) } @@ -140,12 +143,12 @@ func TestListKubeContainers(t *testing.T) { for _, tc := range []struct { name string expected []string - prepare func(*fakeImpl) + prepare func(*FakeImpl) shouldError bool }{ { name: "valid", - prepare: func(mock *fakeImpl) { + prepare: func(mock *FakeImpl) { mock.ListPodSandboxReturns([]*v1.PodSandbox{ {Id: "first"}, {Id: "second"}, @@ -156,7 +159,7 @@ func TestListKubeContainers(t *testing.T) { }, { name: "invalid: list pod sandbox fails", - prepare: func(mock *fakeImpl) { + prepare: func(mock *FakeImpl) { mock.ListPodSandboxReturns(nil, errTest) }, shouldError: true, @@ -164,7 +167,7 @@ func TestListKubeContainers(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { containerRuntime := NewContainerRuntime("") - mock := &fakeImpl{} + mock := &FakeImpl{} if tc.prepare != nil { tc.prepare(mock) } @@ -181,12 +184,12 @@ func TestListKubeContainers(t *testing.T) { func TestSandboxImage(t *testing.T) { for _, tc := range []struct { name, expected string - prepare func(*fakeImpl) + prepare func(*FakeImpl) shouldError bool }{ { name: "valid", - prepare: func(mock *fakeImpl) { + prepare: func(mock *FakeImpl) { mock.StatusReturns(&v1.StatusResponse{Info: map[string]string{ "config": `{"sandboxImage": "pause"}`, }}, nil) @@ -196,14 +199,14 @@ func TestSandboxImage(t *testing.T) { }, { name: "invalid: runtime status fails", - prepare: func(mock *fakeImpl) { + prepare: func(mock *FakeImpl) { mock.StatusReturns(nil, errTest) }, shouldError: true, }, { name: "invalid: no config JSON", - prepare: func(mock *fakeImpl) { + prepare: func(mock *FakeImpl) { mock.StatusReturns(&v1.StatusResponse{Info: map[string]string{ "config": "wrong", }}, nil) @@ -212,7 +215,7 @@ func TestSandboxImage(t *testing.T) { }, { name: "invalid: no config", - prepare: func(mock *fakeImpl) { + prepare: func(mock *FakeImpl) { mock.StatusReturns(&v1.StatusResponse{Info: map[string]string{}}, nil) }, shouldError: true, @@ -220,7 +223,7 @@ func TestSandboxImage(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { containerRuntime := NewContainerRuntime("") - mock := &fakeImpl{} + mock := &FakeImpl{} if tc.prepare != nil { tc.prepare(mock) } @@ -238,7 +241,7 @@ func TestRemoveContainers(t *testing.T) { for _, tc := range []struct { name string containers []string - prepare func(*fakeImpl) + prepare func(*FakeImpl) shouldError bool }{ { @@ -252,7 +255,7 @@ func TestRemoveContainers(t *testing.T) { { name: "invalid: remove pod sandbox fails", containers: []string{"1"}, - prepare: func(mock *fakeImpl) { + prepare: func(mock *FakeImpl) { mock.RemovePodSandboxReturns(errTest) }, shouldError: true, @@ -260,7 +263,7 @@ func TestRemoveContainers(t *testing.T) { { name: "invalid: stop pod sandbox fails", containers: []string{"1"}, - prepare: func(mock *fakeImpl) { + prepare: func(mock *FakeImpl) { mock.StopPodSandboxReturns(errTest) }, shouldError: true, @@ -268,7 +271,7 @@ func TestRemoveContainers(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { containerRuntime := NewContainerRuntime("") - mock := &fakeImpl{} + mock := &FakeImpl{} if tc.prepare != nil { tc.prepare(mock) } @@ -284,7 +287,7 @@ func TestRemoveContainers(t *testing.T) { func TestPullImage(t *testing.T) { for _, tc := range []struct { name string - prepare func(*fakeImpl) + prepare func(*FakeImpl) shouldError bool }{ { @@ -292,7 +295,7 @@ func TestPullImage(t *testing.T) { }, { name: "invalid: pull image fails", - prepare: func(mock *fakeImpl) { + prepare: func(mock *FakeImpl) { mock.PullImageReturns("", errTest) }, shouldError: true, @@ -300,7 +303,7 @@ func TestPullImage(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { containerRuntime := NewContainerRuntime("") - mock := &fakeImpl{} + mock := &FakeImpl{} if tc.prepare != nil { tc.prepare(mock) } @@ -317,11 +320,11 @@ func TestImageExists(t *testing.T) { for _, tc := range []struct { name string expected bool - prepare func(*fakeImpl) + prepare func(*FakeImpl) }{ { name: "valid", - prepare: func(mock *fakeImpl) { + prepare: func(mock *FakeImpl) { mock.ImageStatusReturns(&v1.ImageStatusResponse{ Image: &v1.Image{}, }, nil) @@ -330,7 +333,7 @@ func TestImageExists(t *testing.T) { }, { name: "invalid: image status fails", - prepare: func(mock *fakeImpl) { + prepare: func(mock *FakeImpl) { mock.ImageStatusReturns(nil, errTest) }, expected: false, @@ -338,7 +341,7 @@ func TestImageExists(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { containerRuntime := NewContainerRuntime("") - mock := &fakeImpl{} + mock := &FakeImpl{} if tc.prepare != nil { tc.prepare(mock) } @@ -466,7 +469,7 @@ func TestPullImagesInParallel(t *testing.T) { for _, tc := range []struct { name string ifNotPresent bool - prepare func(*fakeImpl) + prepare func(*FakeImpl) shouldError bool }{ { @@ -478,7 +481,7 @@ func TestPullImagesInParallel(t *testing.T) { }, { name: "invalid: pull fails", - prepare: func(mock *fakeImpl) { + prepare: func(mock *FakeImpl) { mock.PullImageReturns("", errTest) }, shouldError: true, @@ -486,7 +489,7 @@ func TestPullImagesInParallel(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { containerRuntime := NewContainerRuntime("") - mock := &fakeImpl{} + mock := &FakeImpl{} if tc.prepare != nil { tc.prepare(mock) } @@ -498,3 +501,51 @@ func TestPullImagesInParallel(t *testing.T) { }) } } + +func TestIsRuntimeConfigEnabled(t *testing.T) { + for _, tc := range []struct { + name string + prepare func(*FakeImpl) + shouldError bool + expected bool + }{ + { + name: "valid: RuntimeConfig gRPC method is implemented", + prepare: func(mock *FakeImpl) { + mock.RuntimeConfigReturns(&v1.RuntimeConfigResponse{}, nil) + }, + shouldError: false, + expected: true, + }, + { + name: "invalid: RuntimeConfig gRPC method is not implemented", + prepare: func(mock *FakeImpl) { + mock.RuntimeConfigReturns(nil, errNotImplemented) + }, + shouldError: false, + expected: false, + }, + { + name: "invalid: RuntimeConfig gRPC method returns an error", + prepare: func(mock *FakeImpl) { + mock.RuntimeConfigReturns(nil, errTest) + }, + shouldError: true, + expected: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + containerRuntime := NewContainerRuntime("") + mock := &FakeImpl{} + if tc.prepare != nil { + tc.prepare(mock) + } + containerRuntime.SetImpl(mock) + + enabled, err := containerRuntime.IsRuntimeConfigImplemented() + + assert.Equal(t, tc.shouldError, err != nil) + assert.Equal(t, tc.expected, enabled) + }) + } +}