Make kubeadm independent from crictl

With the new `cri-client` staging repository it's finally possible to
decouple `kubeadm` from `crictl`.

Signed-off-by: Sascha Grunert <sgrunert@redhat.com>
This commit is contained in:
Sascha Grunert 2024-05-03 11:10:12 +02:00
parent 1c84623028
commit 7d1bfd9872
No known key found for this signature in database
GPG Key ID: 09D97D153EF94D93
11 changed files with 607 additions and 569 deletions

View File

@ -32,7 +32,6 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/cli-runtime/pkg/genericclioptions"
utilsexec "k8s.io/utils/exec"
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme" kubeadmscheme "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/scheme"
@ -372,8 +371,8 @@ func newCmdConfigImagesPull() *cobra.Command {
if err != nil { if err != nil {
return err return err
} }
containerRuntime, err := utilruntime.NewContainerRuntime(utilsexec.New(), internalcfg.NodeRegistration.CRISocket) containerRuntime := utilruntime.NewContainerRuntime(internalcfg.NodeRegistration.CRISocket)
if err != nil { if err := containerRuntime.Connect(); err != nil {
return err return err
} }
return PullControlPlaneImages(containerRuntime, &internalcfg.ClusterConfiguration) return PullControlPlaneImages(containerRuntime, &internalcfg.ClusterConfiguration)

View File

@ -31,15 +31,11 @@ import (
"github.com/lithammer/dedent" "github.com/lithammer/dedent"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"k8s.io/utils/exec"
fakeexec "k8s.io/utils/exec/testing"
kubeadmapiv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3" kubeadmapiv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
outputapischeme "k8s.io/kubernetes/cmd/kubeadm/app/apis/output/scheme" outputapischeme "k8s.io/kubernetes/cmd/kubeadm/app/apis/output/scheme"
"k8s.io/kubernetes/cmd/kubeadm/app/constants" "k8s.io/kubernetes/cmd/kubeadm/app/constants"
kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util" kubeadmutil "k8s.io/kubernetes/cmd/kubeadm/app/util"
"k8s.io/kubernetes/cmd/kubeadm/app/util/output" "k8s.io/kubernetes/cmd/kubeadm/app/util/output"
utilruntime "k8s.io/kubernetes/cmd/kubeadm/app/util/runtime"
) )
const ( const (
@ -339,46 +335,6 @@ registry.k8s.io/etcd:{{.EtcdVersion}}
} }
} }
func TestImagesPull(t *testing.T) {
fcmd := fakeexec.FakeCmd{
CombinedOutputScript: []fakeexec.FakeAction{
func() ([]byte, []byte, error) { return nil, nil, nil },
func() ([]byte, []byte, error) { return nil, nil, nil },
func() ([]byte, []byte, error) { return nil, nil, nil },
func() ([]byte, []byte, error) { return nil, nil, nil },
func() ([]byte, []byte, error) { return nil, nil, nil },
},
}
fexec := &fakeexec.FakeExec{
CommandScript: []fakeexec.FakeCommandAction{
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
},
LookPathFunc: func(cmd string) (string, error) { return "/usr/bin/crictl", nil },
}
containerRuntime, err := utilruntime.NewContainerRuntime(fexec, constants.DefaultCRISocket)
if err != nil {
t.Errorf("unexpected NewContainerRuntime error: %v", err)
}
images := []string{"a", "b", "c", "d", "a"}
for _, image := range images {
if err := containerRuntime.PullImage(image); err != nil {
t.Fatalf("expected nil but found %v", err)
}
fmt.Printf("[config/images] Pulled %s\n", image)
}
if fcmd.CombinedOutputCalls != len(images) {
t.Errorf("expected %d calls, got %d", len(images), fcmd.CombinedOutputCalls)
}
}
func TestNewCmdConfigPrintActionDefaults(t *testing.T) { func TestNewCmdConfigPrintActionDefaults(t *testing.T) {
tests := []struct { tests := []struct {
name string name string

View File

@ -26,7 +26,6 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"k8s.io/klog/v2" "k8s.io/klog/v2"
utilsexec "k8s.io/utils/exec"
kubeadmapiv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3" kubeadmapiv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
"k8s.io/kubernetes/cmd/kubeadm/app/cmd/options" "k8s.io/kubernetes/cmd/kubeadm/app/cmd/options"
@ -102,7 +101,7 @@ func runCleanupNode(c workflow.RunData) error {
if !r.DryRun() { if !r.DryRun() {
klog.V(1).Info("[reset] Removing Kubernetes-managed containers") klog.V(1).Info("[reset] Removing Kubernetes-managed containers")
if err := removeContainers(utilsexec.New(), r.CRISocketPath()); err != nil { if err := removeContainers(r.CRISocketPath()); err != nil {
klog.Warningf("[reset] Failed to remove containers: %v\n", err) klog.Warningf("[reset] Failed to remove containers: %v\n", err)
} }
} else { } else {
@ -135,9 +134,9 @@ func runCleanupNode(c workflow.RunData) error {
return nil return nil
} }
func removeContainers(execer utilsexec.Interface, criSocketPath string) error { func removeContainers(criSocketPath string) error {
containerRuntime, err := utilruntime.NewContainerRuntime(execer, criSocketPath) containerRuntime := utilruntime.NewContainerRuntime(criSocketPath)
if err != nil { if err := containerRuntime.Connect(); err != nil {
return err return err
} }
containers, err := containerRuntime.ListKubeContainers() containers, err := containerRuntime.ListKubeContainers()

View File

@ -21,9 +21,6 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"k8s.io/utils/exec"
fakeexec "k8s.io/utils/exec/testing"
kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants" kubeadmconstants "k8s.io/kubernetes/cmd/kubeadm/app/constants"
"k8s.io/kubernetes/cmd/kubeadm/app/preflight" "k8s.io/kubernetes/cmd/kubeadm/app/preflight"
) )
@ -212,27 +209,3 @@ func TestConfigDirCleaner(t *testing.T) {
}) })
} }
} }
func TestRemoveContainers(t *testing.T) {
fcmd := fakeexec.FakeCmd{
CombinedOutputScript: []fakeexec.FakeAction{
func() ([]byte, []byte, error) { return []byte("id1\nid2"), nil, nil },
func() ([]byte, []byte, error) { return []byte(""), nil, nil },
func() ([]byte, []byte, error) { return []byte(""), nil, nil },
func() ([]byte, []byte, error) { return []byte(""), nil, nil },
func() ([]byte, []byte, error) { return []byte(""), nil, nil },
},
}
fexec := &fakeexec.FakeExec{
CommandScript: []fakeexec.FakeCommandAction{
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
},
LookPathFunc: func(cmd string) (string, error) { return "/usr/bin/crictl", nil },
}
removeContainers(fexec, "unix:///var/run/crio/crio.sock")
}

View File

@ -857,8 +857,7 @@ func (ipc ImagePullCheck) Check() (warnings, errorList []error) {
for _, image := range ipc.imageList { for _, image := range ipc.imageList {
switch policy { switch policy {
case v1.PullIfNotPresent: case v1.PullIfNotPresent:
ret, err := ipc.runtime.ImageExists(image) if ipc.runtime.ImageExists(image) {
if ret && err == nil {
klog.V(1).Infof("image exists: %s", image) klog.V(1).Infof("image exists: %s", image)
continue continue
} }
@ -1074,8 +1073,8 @@ func RunJoinNodeChecks(execer utilsexec.Interface, cfg *kubeadmapi.JoinConfigura
// addCommonChecks is a helper function to duplicate checks that are common between both the // addCommonChecks is a helper function to duplicate checks that are common between both the
// kubeadm init and join commands // kubeadm init and join commands
func addCommonChecks(execer utilsexec.Interface, k8sVersion string, nodeReg *kubeadmapi.NodeRegistrationOptions, checks []Checker) []Checker { func addCommonChecks(execer utilsexec.Interface, k8sVersion string, nodeReg *kubeadmapi.NodeRegistrationOptions, checks []Checker) []Checker {
containerRuntime, err := utilruntime.NewContainerRuntime(execer, nodeReg.CRISocket) containerRuntime := utilruntime.NewContainerRuntime(nodeReg.CRISocket)
if err != nil { 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) klog.Warningf("[preflight] WARNING: Couldn't create the interface used for talking to the container runtime: %v\n", err)
} else { } else {
checks = append(checks, ContainerRuntimeCheck{runtime: containerRuntime}) checks = append(checks, ContainerRuntimeCheck{runtime: containerRuntime})
@ -1104,8 +1103,8 @@ func RunRootCheckOnly(ignorePreflightErrors sets.Set[string]) error {
// RunPullImagesCheck will pull images kubeadm needs if they are not found on the system // 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 { func RunPullImagesCheck(execer utilsexec.Interface, cfg *kubeadmapi.InitConfiguration, ignorePreflightErrors sets.Set[string]) error {
containerRuntime, err := utilruntime.NewContainerRuntime(utilsexec.New(), cfg.NodeRegistration.CRISocket) containerRuntime := utilruntime.NewContainerRuntime(cfg.NodeRegistration.CRISocket)
if err != nil { if err := containerRuntime.Connect(); err != nil {
return &Error{Msg: err.Error()} return &Error{Msg: err.Error()}
} }

View File

@ -74,7 +74,7 @@ func addSwapCheck(checks []Checker) []Checker {
// addExecChecks adds checks that verify if certain binaries are in PATH // addExecChecks adds checks that verify if certain binaries are in PATH
func addExecChecks(checks []Checker, execer utilsexec.Interface) []Checker { func addExecChecks(checks []Checker, execer utilsexec.Interface) []Checker {
checks = append(checks, checks = append(checks,
InPathCheck{executable: "crictl", mandatory: true, exec: execer}, InPathCheck{executable: "crictl", mandatory: false, exec: execer},
InPathCheck{executable: "conntrack", mandatory: true, exec: execer}, InPathCheck{executable: "conntrack", mandatory: true, exec: execer},
InPathCheck{executable: "ip", mandatory: true, exec: execer}, InPathCheck{executable: "ip", mandatory: true, exec: execer},
InPathCheck{executable: "iptables", mandatory: true, exec: execer}, InPathCheck{executable: "iptables", mandatory: true, exec: execer},

View File

@ -32,7 +32,6 @@ import (
"github.com/lithammer/dedent" "github.com/lithammer/dedent"
"github.com/pkg/errors" "github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/version" "k8s.io/apimachinery/pkg/util/version"
"k8s.io/utils/exec" "k8s.io/utils/exec"
@ -40,9 +39,7 @@ import (
kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm" kubeadmapi "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm"
kubeadmapiv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3" kubeadmapiv1 "k8s.io/kubernetes/cmd/kubeadm/app/apis/kubeadm/v1beta3"
"k8s.io/kubernetes/cmd/kubeadm/app/constants"
configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config" configutil "k8s.io/kubernetes/cmd/kubeadm/app/util/config"
utilruntime "k8s.io/kubernetes/cmd/kubeadm/app/util/runtime"
) )
var ( var (
@ -851,108 +848,6 @@ func TestSetHasItemOrAll(t *testing.T) {
} }
} }
func TestImagePullCheck(t *testing.T) {
fcmd := fakeexec.FakeCmd{
RunScript: []fakeexec.FakeAction{
// Test case 1: img1 and img2 exist, img3 doesn't exist
func() ([]byte, []byte, error) { return nil, nil, nil },
func() ([]byte, []byte, error) { return nil, nil, nil },
func() ([]byte, []byte, error) { return nil, nil, &fakeexec.FakeExitError{Status: 1} },
// Test case 2: images don't exist
func() ([]byte, []byte, error) { return nil, nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return nil, nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return nil, nil, &fakeexec.FakeExitError{Status: 1} },
},
CombinedOutputScript: []fakeexec.FakeAction{
// Test case1: pull only img3
func() ([]byte, []byte, error) { return []byte("pause"), nil, nil },
func() ([]byte, []byte, error) { return nil, nil, nil },
// Test case 2: fail to pull image2 and image3
// If the pull fails, it will be retried 5 times (see PullImageRetry in constants/constants.go)
func() ([]byte, []byte, error) { return []byte("pause"), nil, nil },
func() ([]byte, []byte, error) { return nil, nil, nil },
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
},
}
fexec := &fakeexec.FakeExec{
CommandScript: []fakeexec.FakeCommandAction{
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
func(cmd string, args ...string) exec.Cmd { return fakeexec.InitFakeCmd(&fcmd, cmd, args...) },
},
LookPathFunc: func(cmd string) (string, error) { return "/usr/bin/crictl", nil },
}
containerRuntime, err := utilruntime.NewContainerRuntime(fexec, constants.DefaultCRISocket)
if err != nil {
t.Errorf("unexpected NewContainerRuntime error: %v", err)
}
check := ImagePullCheck{
runtime: containerRuntime,
sandboxImage: "pause",
imageList: []string{"img1", "img2", "img3"},
imagePullPolicy: corev1.PullIfNotPresent,
imagePullSerial: true,
}
warnings, errors := check.Check()
if len(warnings) != 0 {
t.Fatalf("did not expect any warnings but got %q", warnings)
}
if len(errors) != 0 {
t.Fatalf("expected 1 errors but got %d: %q", len(errors), errors)
}
warnings, errors = check.Check()
if len(warnings) != 0 {
t.Fatalf("did not expect any warnings but got %q", warnings)
}
if len(errors) != 2 {
t.Fatalf("expected 2 errors but got %d: %q", len(errors), errors)
}
// Test with unknown policy
check = ImagePullCheck{
runtime: containerRuntime,
sandboxImage: "pause",
imageList: []string{"img1", "img2", "img3"},
imagePullPolicy: "",
imagePullSerial: true,
}
_, errors = check.Check()
if len(errors) != 1 {
t.Fatalf("expected 1 error but got %d: %q", len(errors), errors)
}
}
func TestNumCPUCheck(t *testing.T) { func TestNumCPUCheck(t *testing.T) {
var tests = []struct { var tests = []struct {
numCPU int numCPU int

View File

@ -0,0 +1,71 @@
/*
Copyright 2024 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 runtime
import (
"context"
"time"
criapi "k8s.io/cri-api/pkg/apis"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
criclient "k8s.io/cri-client/pkg"
)
type defaultImpl struct{}
type impl interface {
NewRemoteRuntimeService(endpoint string, connectionTimeout time.Duration) (criapi.RuntimeService, error)
NewRemoteImageService(endpoint string, connectionTimeout time.Duration) (criapi.ImageManagerService, 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
RemovePodSandbox(ctx context.Context, runtimeService criapi.RuntimeService, podSandboxID string) error
PullImage(ctx context.Context, imageService criapi.ImageManagerService, image *runtimeapi.ImageSpec, auth *runtimeapi.AuthConfig, podSandboxConfig *runtimeapi.PodSandboxConfig) (string, error)
ImageStatus(ctx context.Context, imageService criapi.ImageManagerService, image *runtimeapi.ImageSpec, verbose bool) (*runtimeapi.ImageStatusResponse, error)
}
func (*defaultImpl) NewRemoteRuntimeService(endpoint string, connectionTimeout time.Duration) (criapi.RuntimeService, error) {
return criclient.NewRemoteRuntimeService(endpoint, defaultTimeout, nil, nil)
}
func (*defaultImpl) NewRemoteImageService(endpoint string, connectionTimeout time.Duration) (criapi.ImageManagerService, error) {
return criclient.NewRemoteImageService(endpoint, connectionTimeout, nil, nil)
}
func (*defaultImpl) Status(ctx context.Context, runtimeService criapi.RuntimeService, verbose bool) (*runtimeapi.StatusResponse, error) {
return runtimeService.Status(ctx, verbose)
}
func (*defaultImpl) ListPodSandbox(ctx context.Context, runtimeService criapi.RuntimeService, filter *runtimeapi.PodSandboxFilter) ([]*runtimeapi.PodSandbox, error) {
return runtimeService.ListPodSandbox(ctx, filter)
}
func (*defaultImpl) StopPodSandbox(ctx context.Context, runtimeService criapi.RuntimeService, sandboxID string) error {
return runtimeService.StopPodSandbox(ctx, sandboxID)
}
func (*defaultImpl) RemovePodSandbox(ctx context.Context, runtimeService criapi.RuntimeService, podSandboxID string) error {
return runtimeService.RemovePodSandbox(ctx, podSandboxID)
}
func (*defaultImpl) PullImage(ctx context.Context, imageService criapi.ImageManagerService, image *runtimeapi.ImageSpec, auth *runtimeapi.AuthConfig, podSandboxConfig *runtimeapi.PodSandboxConfig) (string, error) {
return imageService.PullImage(ctx, image, auth, podSandboxConfig)
}
func (*defaultImpl) ImageStatus(ctx context.Context, imageService criapi.ImageManagerService, image *runtimeapi.ImageSpec, verbose bool) (*runtimeapi.ImageStatusResponse, error) {
return imageService.ImageStatus(ctx, image, verbose)
}

View File

@ -14,17 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// Package runtime provides the kubeadm container runtime implementation.
package runtime package runtime
import ( import (
"os" "context"
"encoding/json"
"strings" "strings"
"time"
"github.com/pkg/errors" "github.com/pkg/errors"
errorsutil "k8s.io/apimachinery/pkg/util/errors" errorsutil "k8s.io/apimachinery/pkg/util/errors"
criapi "k8s.io/cri-api/pkg/apis"
runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1"
"k8s.io/klog/v2" "k8s.io/klog/v2"
utilsexec "k8s.io/utils/exec"
"k8s.io/kubernetes/cmd/kubeadm/app/constants" "k8s.io/kubernetes/cmd/kubeadm/app/constants"
) )
@ -38,64 +41,94 @@ var defaultKnownCRISockets = []string{
// ContainerRuntime is an interface for working with container runtimes // ContainerRuntime is an interface for working with container runtimes
type ContainerRuntime interface { type ContainerRuntime interface {
Socket() string Connect() error
SetImpl(impl)
IsRunning() error IsRunning() error
ListKubeContainers() ([]string, error) ListKubeContainers() ([]string, error)
RemoveContainers(containers []string) error RemoveContainers(containers []string) error
PullImage(image string) error PullImage(image string) error
PullImagesInParallel(images []string, ifNotPresent bool) error PullImagesInParallel(images []string, ifNotPresent bool) error
ImageExists(image string) (bool, error) ImageExists(image string) bool
SandboxImage() (string, error) SandboxImage() (string, error)
} }
// CRIRuntime is a struct that interfaces with the CRI // CRIRuntime is a struct that interfaces with the CRI
type CRIRuntime struct { type CRIRuntime struct {
exec utilsexec.Interface impl impl
criSocket string criSocket string
crictlPath string runtimeService criapi.RuntimeService
imageService criapi.ImageManagerService
} }
// defaultTimeout is the default timeout inherited by crictl
const defaultTimeout = 2 * time.Second
// NewContainerRuntime sets up and returns a ContainerRuntime struct // NewContainerRuntime sets up and returns a ContainerRuntime struct
func NewContainerRuntime(execer utilsexec.Interface, criSocket string) (ContainerRuntime, error) { func NewContainerRuntime(criSocket string) ContainerRuntime {
const toolName = "crictl" return &CRIRuntime{
crictlPath, err := execer.LookPath(toolName) impl: &defaultImpl{},
criSocket: criSocket,
}
}
// SetImpl can be used to set the internal implementation for testing purposes.
func (runtime *CRIRuntime) SetImpl(impl impl) {
runtime.impl = impl
}
// Connect establishes a connection with the CRI runtime.
func (runtime *CRIRuntime) Connect() error {
runtimeService, err := runtime.impl.NewRemoteRuntimeService(runtime.criSocket, defaultTimeout)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "%s is required by the container runtime", toolName) return errors.Wrap(err, "failed to create new CRI runtime service")
} }
return &CRIRuntime{execer, criSocket, crictlPath}, nil runtime.runtimeService = runtimeService
imageService, err := runtime.impl.NewRemoteImageService(runtime.criSocket, defaultTimeout)
if err != nil {
return errors.Wrap(err, "failed to create new CRI image service")
}
runtime.imageService = imageService
return nil
} }
// Socket returns the CRI socket endpoint // IsRunning checks if runtime is running.
func (runtime *CRIRuntime) Socket() string {
return runtime.criSocket
}
// crictl creates a crictl command for the provided args.
func (runtime *CRIRuntime) crictl(args ...string) utilsexec.Cmd {
cmd := runtime.exec.Command(runtime.crictlPath, append([]string{"-r", runtime.Socket(), "-i", runtime.Socket()}, args...)...)
cmd.SetEnv(os.Environ())
return cmd
}
// IsRunning checks if runtime is running
func (runtime *CRIRuntime) IsRunning() error { func (runtime *CRIRuntime) IsRunning() error {
if out, err := runtime.crictl("info").CombinedOutput(); err != nil { ctx, cancel := defaultContext()
return errors.Wrapf(err, "container runtime is not running: output: %s, error", string(out)) defer cancel()
res, err := runtime.impl.Status(ctx, runtime.runtimeService, false)
if err != nil {
return errors.Wrap(err, "container runtime is not running")
} }
for _, condition := range res.GetStatus().GetConditions() {
if !condition.GetStatus() {
return errors.Errorf(
"container runtime condition %q is not true. reason: %s, message: %s",
condition.GetType(), condition.GetReason(), condition.GetMessage(),
)
}
}
return nil return nil
} }
// ListKubeContainers lists running k8s CRI pods // ListKubeContainers lists running k8s CRI pods
func (runtime *CRIRuntime) ListKubeContainers() ([]string, error) { func (runtime *CRIRuntime) ListKubeContainers() ([]string, error) {
// Disable debug mode regardless how the crictl is configured so that the debug info won't be ctx, cancel := defaultContext()
// iterpreted to the Pod ID. defer cancel()
args := []string{"-D=false", "pods", "-q"}
out, err := runtime.crictl(args...).CombinedOutput() sandboxes, err := runtime.impl.ListPodSandbox(ctx, runtime.runtimeService, nil)
if err != nil { if err != nil {
return nil, errors.Wrapf(err, "output: %s, error", string(out)) return nil, errors.Wrap(err, "failed to list pod sandboxes")
} }
pods := []string{} pods := []string{}
pods = append(pods, strings.Fields(string(out))...) for _, sandbox := range sandboxes {
pods = append(pods, sandbox.GetId())
}
return pods, nil return pods, nil
} }
@ -106,16 +139,23 @@ func (runtime *CRIRuntime) RemoveContainers(containers []string) error {
var lastErr error var lastErr error
for i := 0; i < constants.RemoveContainerRetry; i++ { for i := 0; i < constants.RemoveContainerRetry; i++ {
klog.V(5).Infof("Attempting to remove container %v", container) klog.V(5).Infof("Attempting to remove container %v", container)
out, err := runtime.crictl("stopp", container).CombinedOutput()
if err != nil { ctx, cancel := defaultContext()
lastErr = errors.Wrapf(err, "failed to stop running pod %s: output: %s", container, string(out)) if err := runtime.impl.StopPodSandbox(ctx, runtime.runtimeService, container); err != nil {
lastErr = errors.Wrapf(err, "failed to stop running pod %s", container)
cancel()
continue continue
} }
out, err = runtime.crictl("rmp", container).CombinedOutput() cancel()
if err != nil {
lastErr = errors.Wrapf(err, "failed to remove running container %s: output: %s", container, string(out)) ctx, cancel = defaultContext()
if err := runtime.impl.RemovePodSandbox(ctx, runtime.runtimeService, container); err != nil {
lastErr = errors.Wrapf(err, "failed to remove pod %s", container)
cancel()
continue continue
} }
cancel()
lastErr = nil lastErr = nil
break break
} }
@ -128,16 +168,13 @@ func (runtime *CRIRuntime) RemoveContainers(containers []string) error {
} }
// PullImage pulls the image // PullImage pulls the image
func (runtime *CRIRuntime) PullImage(image string) error { func (runtime *CRIRuntime) PullImage(image string) (err error) {
var err error
var out []byte
for i := 0; i < constants.PullImageRetry; i++ { for i := 0; i < constants.PullImageRetry; i++ {
out, err = runtime.crictl("pull", image).CombinedOutput() if _, err = runtime.impl.PullImage(context.Background(), runtime.imageService, &runtimeapi.ImageSpec{Image: image}, nil, nil); err == nil {
if err == nil {
return nil return nil
} }
} }
return errors.Wrapf(err, "output: %s, error", out) return errors.Wrapf(err, "failed to pull image %s", image)
} }
// PullImagesInParallel pulls a list of images in parallel // PullImagesInParallel pulls a list of images in parallel
@ -146,8 +183,12 @@ func (runtime *CRIRuntime) PullImagesInParallel(images []string, ifNotPresent bo
return errorsutil.NewAggregate(errs) return errorsutil.NewAggregate(errs)
} }
func defaultContext() (context.Context, context.CancelFunc) {
return context.WithTimeout(context.Background(), defaultTimeout)
}
func pullImagesInParallelImpl(images []string, ifNotPresent bool, func pullImagesInParallelImpl(images []string, ifNotPresent bool,
imageExistsFunc func(string) (bool, error), pullImageFunc func(string) error) []error { imageExistsFunc func(string) bool, pullImageFunc func(string) error) []error {
var errs []error var errs []error
errChan := make(chan error, len(images)) errChan := make(chan error, len(images))
@ -157,11 +198,7 @@ func pullImagesInParallelImpl(images []string, ifNotPresent bool,
image := img image := img
go func() { go func() {
if ifNotPresent { if ifNotPresent {
exists, err := imageExistsFunc(image) exists := imageExistsFunc(image)
if err != nil {
errChan <- errors.WithMessagef(err, "failed to check if image %s exists", image)
return
}
if exists { if exists {
klog.V(1).Infof("image exists: %s", image) klog.V(1).Infof("image exists: %s", image)
errChan <- nil errChan <- nil
@ -188,9 +225,11 @@ func pullImagesInParallelImpl(images []string, ifNotPresent bool,
} }
// ImageExists checks to see if the image exists on the system // ImageExists checks to see if the image exists on the system
func (runtime *CRIRuntime) ImageExists(image string) (bool, error) { func (runtime *CRIRuntime) ImageExists(image string) bool {
err := runtime.crictl("inspecti", image).Run() ctx, cancel := defaultContext()
return err == nil, nil defer cancel()
_, err := runtime.impl.ImageStatus(ctx, runtime.imageService, &runtimeapi.ImageSpec{Image: image}, false)
return err == nil
} }
// detectCRISocketImpl is separated out only for test purposes, DON'T call it directly, use DetectCRISocket instead // detectCRISocketImpl is separated out only for test purposes, DON'T call it directly, use DetectCRISocket instead
@ -212,7 +251,7 @@ func detectCRISocketImpl(isSocket func(string) bool, knownCRISockets []string) (
return foundCRISockets[0], nil return foundCRISockets[0], nil
default: default:
// Multiple CRIs installed? // Multiple CRIs installed?
return "", errors.Errorf("Found multiple CRI endpoints on the host. Please define which one do you wish "+ return "", errors.Errorf("found multiple CRI endpoints on the host. Please define which one do you wish "+
"to use by setting the 'criSocket' field in the kubeadm configuration file: %s", "to use by setting the 'criSocket' field in the kubeadm configuration file: %s",
strings.Join(foundCRISockets, ", ")) strings.Join(foundCRISockets, ", "))
} }
@ -225,16 +264,26 @@ func DetectCRISocket() (string, error) {
// SandboxImage returns the sandbox image used by the container runtime // SandboxImage returns the sandbox image used by the container runtime
func (runtime *CRIRuntime) SandboxImage() (string, error) { func (runtime *CRIRuntime) SandboxImage() (string, error) {
args := []string{"-D=false", "info", "-o", "go-template", "--template", "{{.config.sandboxImage}}"} ctx, cancel := defaultContext()
out, err := runtime.crictl(args...).CombinedOutput() defer cancel()
status, err := runtime.impl.Status(ctx, runtime.runtimeService, true)
if err != nil { if err != nil {
return "", errors.Wrapf(err, "output: %s, error", string(out)) return "", errors.Wrap(err, "failed to get runtime status")
} }
sandboxImage := strings.TrimSpace(string(out)) infoConfig, ok := status.GetInfo()["config"]
if len(sandboxImage) > 0 { if !ok {
return sandboxImage, nil return "", errors.Errorf("no 'config' field in CRI info: %+v", status)
} }
return "", errors.Errorf("the detected sandbox image is empty") type config struct {
SandboxImage string `json:"sandboxImage,omitempty"`
}
c := config{}
if err := json.Unmarshal([]byte(infoConfig), &c); err != nil {
return "", errors.Wrap(err, "failed to unmarshal CRI info config")
}
return c.SandboxImage, nil
} }

View File

@ -0,0 +1,152 @@
/*
Copyright 2024 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 runtime
import (
"context"
"time"
cri "k8s.io/cri-api/pkg/apis"
v1 "k8s.io/cri-api/pkg/apis/runtime/v1"
)
type fakeImpl struct {
imageStatusReturns struct {
res *v1.ImageStatusResponse
err error
}
listPodSandboxReturns struct {
res []*v1.PodSandbox
err error
}
newRemoteImageServiceReturns struct {
res cri.ImageManagerService
err error
}
newRemoteRuntimeServiceReturns struct {
res cri.RuntimeService
err error
}
pullImageReturns struct {
res string
err error
}
removePodSandboxReturns struct {
res error
}
statusReturns struct {
res *v1.StatusResponse
err error
}
stopPodSandboxReturns struct {
res error
}
}
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) {
fake.imageStatusReturns = struct {
res *v1.ImageStatusResponse
err error
}{res, err}
}
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) {
fake.listPodSandboxReturns = struct {
res []*v1.PodSandbox
err error
}{res, err}
}
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) {
fake.newRemoteImageServiceReturns = struct {
res cri.ImageManagerService
err error
}{res, err}
}
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) {
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) {
fakeReturns := fake.pullImageReturns
return fakeReturns.res, fakeReturns.err
}
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 {
fakeReturns := fake.removePodSandboxReturns
return fakeReturns.res
}
func (fake *fakeImpl) RemovePodSandboxReturns(res error) {
fake.removePodSandboxReturns = struct {
res error
}{res}
}
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) {
fake.statusReturns = struct {
res *v1.StatusResponse
err error
}{res, err}
}
func (fake *fakeImpl) StopPodSandbox(context.Context, cri.RuntimeService, string) error {
fakeReturns := fake.stopPodSandboxReturns
return fakeReturns.res
}
func (fake *fakeImpl) StopPodSandboxReturns(res error) {
fake.stopPodSandboxReturns = struct {
res error
}{res}
}

View File

@ -17,331 +17,314 @@ limitations under the License.
package runtime package runtime
import ( import (
"errors"
"net" "net"
"os" "os"
"reflect"
"runtime" "runtime"
"testing" "testing"
"github.com/pkg/errors" "github.com/stretchr/testify/assert"
"k8s.io/utils/exec" v1 "k8s.io/cri-api/pkg/apis/runtime/v1"
fakeexec "k8s.io/utils/exec/testing"
"k8s.io/kubernetes/cmd/kubeadm/app/constants" "k8s.io/kubernetes/cmd/kubeadm/app/constants"
) )
var errTest = errors.New("test")
func TestNewContainerRuntime(t *testing.T) { func TestNewContainerRuntime(t *testing.T) {
execLookPathOK := &fakeexec.FakeExec{ for _, tc := range []struct {
LookPathFunc: func(cmd string) (string, error) { return "/usr/bin/crictl", nil },
}
execLookPathErr := &fakeexec.FakeExec{
LookPathFunc: func(cmd string) (string, error) { return "", errors.Errorf("%s not found", cmd) },
}
cases := []struct {
name string name string
execer *fakeexec.FakeExec prepare func(*fakeImpl)
isError bool shouldError bool
}{ }{
{"valid: crictl present", execLookPathOK, false}, {
{"invalid: no crictl", execLookPathErr, true}, name: "valid",
} shouldError: false,
},
for _, tc := range cases { {
name: "invalid: new runtime service fails",
prepare: func(mock *fakeImpl) {
mock.NewRemoteRuntimeServiceReturns(nil, errTest)
},
shouldError: true,
},
{
name: "invalid: new image service fails",
prepare: func(mock *fakeImpl) {
mock.NewRemoteImageServiceReturns(nil, errTest)
},
shouldError: true,
},
} {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
_, err := NewContainerRuntime(tc.execer, "unix:///some/socket.sock") containerRuntime := NewContainerRuntime("")
if err != nil { mock := &fakeImpl{}
if !tc.isError { if tc.prepare != nil {
t.Fatalf("unexpected NewContainerRuntime error. error: %v", err) tc.prepare(mock)
} }
return // expected error occurs, impossible to test runtime further containerRuntime.SetImpl(mock)
}
if tc.isError && err == nil {
t.Fatal("unexpected NewContainerRuntime success")
}
})
}
}
func genFakeActions(fcmd *fakeexec.FakeCmd, num int) []fakeexec.FakeCommandAction { err := containerRuntime.Connect()
var actions []fakeexec.FakeCommandAction
for i := 0; i < num; i++ { assert.Equal(t, tc.shouldError, err != nil)
actions = append(actions, func(cmd string, args ...string) exec.Cmd {
return fakeexec.InitFakeCmd(fcmd, cmd, args...)
}) })
} }
return actions
} }
func TestIsRunning(t *testing.T) { func TestIsRunning(t *testing.T) {
fcmd := fakeexec.FakeCmd{ for _, tc := range []struct {
CombinedOutputScript: []fakeexec.FakeAction{
func() ([]byte, []byte, error) { return nil, nil, nil },
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return nil, nil, nil },
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
},
}
criExecer := &fakeexec.FakeExec{
CommandScript: genFakeActions(&fcmd, len(fcmd.CombinedOutputScript)),
LookPathFunc: func(cmd string) (string, error) { return "/usr/bin/crictl", nil },
}
cases := []struct {
name string name string
criSocket string prepare func(*fakeImpl)
execer *fakeexec.FakeExec shouldError bool
isError bool
}{ }{
{"valid: CRI-O is running", "unix:///var/run/crio/crio.sock", criExecer, false}, {
{"invalid: CRI-O is not running", "unix:///var/run/crio/crio.sock", criExecer, true}, name: "valid",
} shouldError: false,
},
for _, tc := range cases { {
name: "invalid: runtime status fails",
prepare: func(mock *fakeImpl) {
mock.StatusReturns(nil, errTest)
},
shouldError: true,
},
{
name: "invalid: runtime condition status not 'true'",
prepare: func(mock *fakeImpl) {
mock.StatusReturns(&v1.StatusResponse{Status: &v1.RuntimeStatus{
Conditions: []*v1.RuntimeCondition{
{
Status: false,
},
},
},
}, nil)
},
shouldError: true,
},
} {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
runtime, err := NewContainerRuntime(tc.execer, tc.criSocket) containerRuntime := NewContainerRuntime("")
if err != nil { mock := &fakeImpl{}
t.Fatalf("unexpected NewContainerRuntime error: %v", err) if tc.prepare != nil {
} tc.prepare(mock)
isRunning := runtime.IsRunning()
if tc.isError && isRunning == nil {
t.Error("unexpected IsRunning() success")
}
if !tc.isError && isRunning != nil {
t.Error("unexpected IsRunning() error")
} }
containerRuntime.SetImpl(mock)
err := containerRuntime.IsRunning()
assert.Equal(t, tc.shouldError, err != nil)
}) })
} }
} }
func TestListKubeContainers(t *testing.T) { func TestListKubeContainers(t *testing.T) {
fcmd := fakeexec.FakeCmd{ for _, tc := range []struct {
CombinedOutputScript: []fakeexec.FakeAction{
func() ([]byte, []byte, error) { return []byte("k8s_p1\nk8s_p2"), nil, nil },
func() ([]byte, []byte, error) { return nil, nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return []byte("k8s_p1\nk8s_p2"), nil, nil },
},
}
execer := &fakeexec.FakeExec{
CommandScript: genFakeActions(&fcmd, len(fcmd.CombinedOutputScript)),
LookPathFunc: func(cmd string) (string, error) { return "/usr/bin/crictl", nil },
}
cases := []struct {
name string name string
criSocket string expected []string
isError bool prepare func(*fakeImpl)
shouldError bool
}{ }{
{"valid: list containers using CRI socket url", "unix:///var/run/crio/crio.sock", false}, {
{"invalid: list containers using CRI socket url", "unix:///var/run/crio/crio.sock", true}, name: "valid",
} prepare: func(mock *fakeImpl) {
mock.ListPodSandboxReturns([]*v1.PodSandbox{
for _, tc := range cases { {Id: "first"},
{Id: "second"},
}, nil)
},
expected: []string{"first", "second"},
shouldError: false,
},
{
name: "invalid: list pod sandbox fails",
prepare: func(mock *fakeImpl) {
mock.ListPodSandboxReturns(nil, errTest)
},
shouldError: true,
},
} {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
runtime, err := NewContainerRuntime(execer, tc.criSocket) containerRuntime := NewContainerRuntime("")
if err != nil { mock := &fakeImpl{}
t.Fatalf("unexpected NewContainerRuntime error: %v", err) if tc.prepare != nil {
tc.prepare(mock)
} }
containerRuntime.SetImpl(mock)
containers, err := runtime.ListKubeContainers() containers, err := containerRuntime.ListKubeContainers()
if tc.isError {
if err == nil {
t.Errorf("unexpected ListKubeContainers success")
}
return
} else if err != nil {
t.Errorf("unexpected ListKubeContainers error: %v", err)
}
if !reflect.DeepEqual(containers, []string{"k8s_p1", "k8s_p2"}) { assert.Equal(t, tc.shouldError, err != nil)
t.Errorf("unexpected ListKubeContainers output: %v", containers) assert.Equal(t, tc.expected, containers)
}
}) })
} }
} }
func TestSandboxImage(t *testing.T) { func TestSandboxImage(t *testing.T) {
fcmd := fakeexec.FakeCmd{ for _, tc := range []struct {
CombinedOutputScript: []fakeexec.FakeAction{ name, expected string
func() ([]byte, []byte, error) { return []byte("registry.k8s.io/pause:ver"), nil, nil }, prepare func(*fakeImpl)
func() ([]byte, []byte, error) { return []byte("registry.k8s.io/pause:ver\n"), nil, nil }, shouldError bool
func() ([]byte, []byte, error) { return nil, nil, nil },
func() ([]byte, []byte, error) { return nil, nil, &fakeexec.FakeExitError{Status: 1} },
},
}
execer := &fakeexec.FakeExec{
CommandScript: genFakeActions(&fcmd, len(fcmd.CombinedOutputScript)),
LookPathFunc: func(cmd string) (string, error) { return "/usr/bin/crictl", nil },
}
cases := []struct {
name string
expected string
isError bool
}{ }{
{"valid: read sandbox image normally", "registry.k8s.io/pause:ver", false}, {
{"valid: read sandbox image with leading/trailing white spaces", "registry.k8s.io/pause:ver", false}, name: "valid",
{"invalid: read empty sandbox image", "", true}, prepare: func(mock *fakeImpl) {
{"invalid: failed to read sandbox image", "", true}, mock.StatusReturns(&v1.StatusResponse{Info: map[string]string{
} "config": `{"sandboxImage": "pause"}`,
}}, nil)
for _, tc := range cases { },
expected: "pause",
shouldError: false,
},
{
name: "invalid: runtime status fails",
prepare: func(mock *fakeImpl) {
mock.StatusReturns(nil, errTest)
},
shouldError: true,
},
{
name: "invalid: no config JSON",
prepare: func(mock *fakeImpl) {
mock.StatusReturns(&v1.StatusResponse{Info: map[string]string{
"config": "wrong",
}}, nil)
},
shouldError: true,
},
{
name: "invalid: no config",
prepare: func(mock *fakeImpl) {
mock.StatusReturns(&v1.StatusResponse{Info: map[string]string{}}, nil)
},
shouldError: true,
},
} {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
runtime, err := NewContainerRuntime(execer, "unix:///some/socket.sock") containerRuntime := NewContainerRuntime("")
if err != nil { mock := &fakeImpl{}
t.Fatalf("unexpected NewContainerRuntime error: %v", err) if tc.prepare != nil {
tc.prepare(mock)
} }
containerRuntime.SetImpl(mock)
sandboxImage, err := runtime.SandboxImage() image, err := containerRuntime.SandboxImage()
if tc.isError {
if err == nil {
t.Errorf("unexpected SandboxImage success")
}
return
} else if err != nil {
t.Errorf("unexpected SandboxImage error: %v", err)
}
if sandboxImage != tc.expected { assert.Equal(t, tc.shouldError, err != nil)
t.Errorf("expected sandbox image %v, but got %v", tc.expected, sandboxImage) assert.Equal(t, tc.expected, image)
}
}) })
} }
} }
func TestRemoveContainers(t *testing.T) { func TestRemoveContainers(t *testing.T) {
fakeOK := func() ([]byte, []byte, error) { return nil, nil, nil } for _, tc := range []struct {
fakeErr := func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} }
fcmd := fakeexec.FakeCmd{
CombinedOutputScript: []fakeexec.FakeAction{
fakeOK, fakeOK, fakeOK, fakeOK, fakeOK, fakeOK, // Test case 1
fakeOK, fakeOK, fakeOK, fakeErr, fakeOK, fakeErr, fakeOK, fakeErr, fakeOK, fakeErr, fakeOK, fakeErr, fakeOK, fakeOK, // Test case 2
fakeErr, fakeErr, fakeErr, fakeErr, fakeErr, fakeOK, fakeOK, fakeOK, fakeOK, // Test case 3
},
}
execer := &fakeexec.FakeExec{
CommandScript: genFakeActions(&fcmd, len(fcmd.CombinedOutputScript)),
LookPathFunc: func(cmd string) (string, error) { return "/usr/bin/crictl", nil },
}
cases := []struct {
name string name string
criSocket string
containers []string containers []string
isError bool prepare func(*fakeImpl)
shouldError bool
}{ }{
{"valid: remove containers using CRI", "unix:///var/run/crio/crio.sock", []string{"k8s_p1", "k8s_p2", "k8s_p3"}, false}, // Test case 1 {
{"invalid: CRI rmp failure", "unix:///var/run/crio/crio.sock", []string{"k8s_p1", "k8s_p2", "k8s_p3"}, true}, // Test case 2 name: "valid",
{"invalid: CRI stopp failure", "unix:///var/run/crio/crio.sock", []string{"k8s_p1", "k8s_p2", "k8s_p3"}, true}, // Test case 3 },
} {
name: "valid: two containers",
for _, tc := range cases { containers: []string{"1", "2"},
shouldError: false,
},
{
name: "invalid: remove pod sandbox fails",
containers: []string{"1"},
prepare: func(mock *fakeImpl) {
mock.RemovePodSandboxReturns(errTest)
},
shouldError: true,
},
{
name: "invalid: stop pod sandbox fails",
containers: []string{"1"},
prepare: func(mock *fakeImpl) {
mock.StopPodSandboxReturns(errTest)
},
shouldError: true,
},
} {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
runtime, err := NewContainerRuntime(execer, tc.criSocket) containerRuntime := NewContainerRuntime("")
if err != nil { mock := &fakeImpl{}
t.Fatalf("unexpected NewContainerRuntime error: %v, criSocket: %s", err, tc.criSocket) if tc.prepare != nil {
tc.prepare(mock)
} }
containerRuntime.SetImpl(mock)
err = runtime.RemoveContainers(tc.containers) err := containerRuntime.RemoveContainers(tc.containers)
if !tc.isError && err != nil {
t.Errorf("unexpected RemoveContainers errors: %v, criSocket: %s, containers: %v", err, tc.criSocket, tc.containers) assert.Equal(t, tc.shouldError, err != nil)
}
if tc.isError && err == nil {
t.Errorf("unexpected RemoveContainers success, criSocket: %s, containers: %v", tc.criSocket, tc.containers)
}
}) })
} }
} }
func TestPullImage(t *testing.T) { func TestPullImage(t *testing.T) {
fcmd := fakeexec.FakeCmd{ for _, tc := range []struct {
CombinedOutputScript: []fakeexec.FakeAction{
func() ([]byte, []byte, error) { return nil, nil, nil },
// If the pull fails, it will be retried 5 times (see PullImageRetry in constants/constants.go)
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return nil, nil, nil },
// If the pull fails, it will be retried 5 times (see PullImageRetry in constants/constants.go)
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return []byte("error"), nil, &fakeexec.FakeExitError{Status: 1} },
},
}
execer := &fakeexec.FakeExec{
CommandScript: genFakeActions(&fcmd, len(fcmd.CombinedOutputScript)),
LookPathFunc: func(cmd string) (string, error) { return "/usr/bin/crictl", nil },
}
cases := []struct {
name string name string
criSocket string prepare func(*fakeImpl)
image string shouldError bool
isError bool
}{ }{
{"valid: pull image using CRI", "unix:///var/run/crio/crio.sock", "image1", false}, {
{"invalid: CRI pull error", "unix:///var/run/crio/crio.sock", "image2", true}, name: "valid",
} },
{
for _, tc := range cases { name: "invalid: pull image fails",
prepare: func(mock *fakeImpl) {
mock.PullImageReturns("", errTest)
},
shouldError: true,
},
} {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
runtime, err := NewContainerRuntime(execer, tc.criSocket) containerRuntime := NewContainerRuntime("")
if err != nil { mock := &fakeImpl{}
t.Fatalf("unexpected NewContainerRuntime error: %v, criSocket: %s", err, tc.criSocket) if tc.prepare != nil {
tc.prepare(mock)
} }
containerRuntime.SetImpl(mock)
err = runtime.PullImage(tc.image) err := containerRuntime.PullImage("")
if !tc.isError && err != nil {
t.Errorf("unexpected PullImage error: %v, criSocket: %s, image: %s", err, tc.criSocket, tc.image) assert.Equal(t, tc.shouldError, err != nil)
}
if tc.isError && err == nil {
t.Errorf("unexpected PullImage success, criSocket: %s, image: %s", tc.criSocket, tc.image)
}
}) })
} }
} }
func TestImageExists(t *testing.T) { func TestImageExists(t *testing.T) {
fcmd := fakeexec.FakeCmd{ for _, tc := range []struct {
RunScript: []fakeexec.FakeAction{
func() ([]byte, []byte, error) { return nil, nil, nil },
func() ([]byte, []byte, error) { return nil, nil, &fakeexec.FakeExitError{Status: 1} },
func() ([]byte, []byte, error) { return nil, nil, nil },
func() ([]byte, []byte, error) { return nil, nil, &fakeexec.FakeExitError{Status: 1} },
},
}
execer := &fakeexec.FakeExec{
CommandScript: genFakeActions(&fcmd, len(fcmd.RunScript)),
LookPathFunc: func(cmd string) (string, error) { return "/usr/bin/crictl", nil },
}
cases := []struct {
name string name string
criSocket string expected bool
image string prepare func(*fakeImpl)
result bool
}{ }{
{"valid: test if image exists using CRI", "unix:///var/run/crio/crio.sock", "image1", false}, {
{"invalid: CRI inspect failure", "unix:///var/run/crio/crio.sock", "image2", true}, name: "valid",
} expected: true,
},
for _, tc := range cases { {
name: "invalid: image status fails",
prepare: func(mock *fakeImpl) {
mock.ImageStatusReturns(nil, errTest)
},
expected: false,
},
} {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
runtime, err := NewContainerRuntime(execer, tc.criSocket) containerRuntime := NewContainerRuntime("")
if err != nil { mock := &fakeImpl{}
t.Fatalf("unexpected NewContainerRuntime error: %v, criSocket: %s", err, tc.criSocket) if tc.prepare != nil {
tc.prepare(mock)
} }
containerRuntime.SetImpl(mock)
result, err := runtime.ImageExists(tc.image) exists := containerRuntime.ImageExists("")
if !tc.result != result {
t.Errorf("unexpected ImageExists result: %t, criSocket: %s, image: %s, expected result: %t", err, tc.criSocket, tc.image, tc.result) assert.Equal(t, tc.expected, exists)
}
}) })
} }
} }
@ -462,77 +445,39 @@ func TestDetectCRISocketImpl(t *testing.T) {
} }
} }
func TestPullImagesInParallelImpl(t *testing.T) { func TestPullImagesInParallel(t *testing.T) {
testError := errors.New("error") for _, tc := range []struct {
tests := []struct {
name string name string
images []string
ifNotPresent bool ifNotPresent bool
imageExistsFunc func(string) (bool, error) prepare func(*fakeImpl)
pullImageFunc func(string) error shouldError bool
expectedErrors int
}{ }{
{ {
name: "all images exist, no errors", name: "valid",
images: []string{"foo", "bar", "baz"},
ifNotPresent: true,
imageExistsFunc: func(string) (bool, error) {
return true, nil
},
pullImageFunc: nil,
expectedErrors: 0,
}, },
{ {
name: "cannot check if one image exists due to error", name: "valid: ifNotPresent is true",
images: []string{"foo", "bar", "baz"},
ifNotPresent: true, ifNotPresent: true,
imageExistsFunc: func(image string) (bool, error) {
if image == "baz" {
return false, testError
}
return true, nil
},
pullImageFunc: nil,
expectedErrors: 1,
}, },
{ {
name: "cannot pull two images", name: "invalid: pull fails",
images: []string{"foo", "bar", "baz"}, prepare: func(mock *fakeImpl) {
ifNotPresent: true, mock.PullImageReturns("", errTest)
imageExistsFunc: func(string) (bool, error) {
return false, nil
}, },
pullImageFunc: func(image string) error { shouldError: true,
if image == "foo" {
return nil
}
return testError
}, },
expectedErrors: 2, } {
},
{
name: "pull all images",
images: []string{"foo", "bar", "baz"},
ifNotPresent: true,
imageExistsFunc: func(string) (bool, error) {
return false, nil
},
pullImageFunc: func(string) error {
return nil
},
expectedErrors: 0,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
actual := pullImagesInParallelImpl(tc.images, tc.ifNotPresent, containerRuntime := NewContainerRuntime("")
tc.imageExistsFunc, tc.pullImageFunc) mock := &fakeImpl{}
if len(actual) != tc.expectedErrors { if tc.prepare != nil {
t.Fatalf("expected non-nil errors: %v, got: %v, full list of errors: %v", tc.prepare(mock)
tc.expectedErrors, len(actual), actual)
} }
containerRuntime.SetImpl(mock)
err := containerRuntime.PullImagesInParallel([]string{"first", "second"}, tc.ifNotPresent)
assert.Equal(t, tc.shouldError, err != nil)
}) })
} }
} }