kubelet: Update engine version parsing to handle semantic versioning

This updates the dockertools.dockerVersion to use a semantic versioning
library to more gracefully support engine versions which include
additional version fields.

Previously, go-dockerclient's APIVersion struct was use which only
handles plain numeric x.y.z version strings. With #19675, the library
was now used on the Docker engine string, however it is possible for the
engine string to include include additional information for beta, rc, or
distro specific builds.

This PR also enables the TestDockerRuntimeVersion test which was
previously just a FIXME and updates it to pass, and be used to test the
version string that cause #20005.

This negates the need for fsouza/go-dockerclient#451, since even with
that change, if a user was running Docker 1.10.0-rc1, this would cause
the kubelet to report it as simply 1.10.0.
This commit is contained in:
Ken Robertson 2016-01-22 12:54:23 -08:00
parent 0b00928c74
commit fff8a7c371
2 changed files with 94 additions and 24 deletions

View File

@ -31,6 +31,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/coreos/go-semver/semver"
docker "github.com/fsouza/go-dockerclient" docker "github.com/fsouza/go-dockerclient"
"github.com/golang/glog" "github.com/golang/glog"
"github.com/golang/groupcache/lru" "github.com/golang/groupcache/lru"
@ -944,18 +945,47 @@ func (dm *DockerManager) podInfraContainerChanged(pod *api.Pod, podInfraContaine
return podInfraContainerStatus.Hash != kubecontainer.HashContainer(expectedPodInfraContainer), nil return podInfraContainerStatus.Hash != kubecontainer.HashContainer(expectedPodInfraContainer), nil
} }
type dockerVersion docker.APIVersion // dockerVersion implementes kubecontainer.Version interface by implementing
// Compare() and String() (which is implemented by the underlying semver.Version)
func NewVersion(input string) (dockerVersion, error) { // TODO: this code is the same as rktVersion and may make sense to be moved to
version, err := docker.NewAPIVersion(input) // somewhere shared.
return dockerVersion(version), err type dockerVersion struct {
*semver.Version
} }
func (dv dockerVersion) String() string { func newDockerVersion(version string) (dockerVersion, error) {
sem, err := semver.NewVersion(version)
if err != nil {
return dockerVersion{}, err
}
return dockerVersion{sem}, nil
}
func (r dockerVersion) Compare(other string) (int, error) {
v, err := semver.NewVersion(other)
if err != nil {
return -1, err
}
if r.LessThan(*v) {
return -1, nil
}
if v.LessThan(*r.Version) {
return 1, nil
}
return 0, nil
}
// dockerVersion implementes kubecontainer.Version interface by implementing
// Compare() and String() on top og go-dockerclient's APIVersion. This version
// string doesn't conform to semantic versioning, as it is only "x.y"
type dockerAPIVersion docker.APIVersion
func (dv dockerAPIVersion) String() string {
return docker.APIVersion(dv).String() return docker.APIVersion(dv).String()
} }
func (dv dockerVersion) Compare(other string) (int, error) { func (dv dockerAPIVersion) Compare(other string) (int, error) {
a := docker.APIVersion(dv) a := docker.APIVersion(dv)
b, err := docker.NewAPIVersion(other) b, err := docker.NewAPIVersion(other)
if err != nil { if err != nil {
@ -981,12 +1011,12 @@ func (dm *DockerManager) Version() (kubecontainer.Version, error) {
} }
engineVersion := env.Get("Version") engineVersion := env.Get("Version")
version, err := docker.NewAPIVersion(engineVersion) version, err := newDockerVersion(engineVersion)
if err != nil { if err != nil {
glog.Errorf("docker: failed to parse docker server version %q: %v", engineVersion, err) glog.Errorf("docker: failed to parse docker server version %q: %v", engineVersion, err)
return nil, fmt.Errorf("docker: failed to parse docker server version %q: %v", engineVersion, err) return nil, fmt.Errorf("docker: failed to parse docker server version %q: %v", engineVersion, err)
} }
return dockerVersion(version), nil return version, nil
} }
func (dm *DockerManager) APIVersion() (kubecontainer.Version, error) { func (dm *DockerManager) APIVersion() (kubecontainer.Version, error) {
@ -1001,7 +1031,7 @@ func (dm *DockerManager) APIVersion() (kubecontainer.Version, error) {
glog.Errorf("docker: failed to parse docker api version %q: %v", apiVersion, err) glog.Errorf("docker: failed to parse docker api version %q: %v", apiVersion, err)
return nil, fmt.Errorf("docker: failed to parse docker api version %q: %v", apiVersion, err) return nil, fmt.Errorf("docker: failed to parse docker api version %q: %v", apiVersion, err)
} }
return dockerVersion(version), nil return dockerAPIVersion(version), nil
} }
// The first version of docker that supports exec natively is 1.3.0 == API 1.15 // The first version of docker that supports exec natively is 1.3.0 == API 1.15

View File

@ -2719,26 +2719,59 @@ func TestUpdateNewNodeOutOfDiskStatusWithTransitionFrequency(t *testing.T) {
} }
} }
// FIXME: Enable me.. func TestDockerRuntimeVersion(t *testing.T) {
func testDockerRuntimeVersion(t *testing.T) {
testKubelet := newTestKubelet(t) testKubelet := newTestKubelet(t)
kubelet := testKubelet.kubelet kubelet := testKubelet.kubelet
fakeRuntime := testKubelet.fakeRuntime fakeRuntime := testKubelet.fakeRuntime
fakeRuntime.RuntimeType = "docker" fakeRuntime.RuntimeType = "docker"
fakeRuntime.VersionInfo = "1.5.0" fakeRuntime.VersionInfo = "1.10.0-rc1-fc24"
fakeRuntime.APIVersionInfo = "1.18" fakeRuntime.APIVersionInfo = "1.22"
kubeClient := testKubelet.fakeKubeClient kubeClient := testKubelet.fakeKubeClient
kubeClient.ReactionChain = testclient.NewSimpleFake(&api.NodeList{Items: []api.Node{ kubeClient.ReactionChain = testclient.NewSimpleFake(&api.NodeList{Items: []api.Node{
{ObjectMeta: api.ObjectMeta{Name: testKubeletHostname}}, {
ObjectMeta: api.ObjectMeta{Name: testKubeletHostname},
Spec: api.NodeSpec{},
Status: api.NodeStatus{
Conditions: []api.NodeCondition{
{
Type: api.NodeOutOfDisk,
Status: api.ConditionFalse,
Reason: "KubeletHasSufficientDisk",
Message: fmt.Sprintf("kubelet has sufficient disk space available"),
LastHeartbeatTime: unversioned.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC),
LastTransitionTime: unversioned.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC),
},
{
Type: api.NodeReady,
Status: api.ConditionTrue,
Reason: "KubeletReady",
Message: fmt.Sprintf("kubelet is posting ready status"),
LastHeartbeatTime: unversioned.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC),
LastTransitionTime: unversioned.Date(2012, 1, 1, 0, 0, 0, 0, time.UTC),
},
},
Capacity: api.ResourceList{
api.ResourceCPU: *resource.NewMilliQuantity(3000, resource.DecimalSI),
api.ResourceMemory: *resource.NewQuantity(20E9, resource.BinarySI),
api.ResourcePods: *resource.NewQuantity(0, resource.DecimalSI),
},
Allocatable: api.ResourceList{
api.ResourceCPU: *resource.NewMilliQuantity(2800, resource.DecimalSI),
api.ResourceMemory: *resource.NewQuantity(19900E6, resource.BinarySI),
api.ResourcePods: *resource.NewQuantity(0, resource.DecimalSI),
},
},
},
}}).ReactionChain }}).ReactionChain
mockCadvisor := testKubelet.fakeCadvisor
mockCadvisor.On("Start").Return(nil)
machineInfo := &cadvisorapi.MachineInfo{ machineInfo := &cadvisorapi.MachineInfo{
MachineID: "123", MachineID: "123",
SystemUUID: "abc", SystemUUID: "abc",
BootID: "1b3", BootID: "1b3",
NumCores: 2, NumCores: 2,
MemoryCapacity: 1024, MemoryCapacity: 20E9,
} }
mockCadvisor := testKubelet.fakeCadvisor
mockCadvisor.On("MachineInfo").Return(machineInfo, nil) mockCadvisor.On("MachineInfo").Return(machineInfo, nil)
versionInfo := &cadvisorapi.VersionInfo{ versionInfo := &cadvisorapi.VersionInfo{
KernelVersion: "3.16.0-0.bpo.4-amd64", KernelVersion: "3.16.0-0.bpo.4-amd64",
@ -2779,13 +2812,18 @@ func testDockerRuntimeVersion(t *testing.T) {
BootID: "1b3", BootID: "1b3",
KernelVersion: "3.16.0-0.bpo.4-amd64", KernelVersion: "3.16.0-0.bpo.4-amd64",
OSImage: "Debian GNU/Linux 7 (wheezy)", OSImage: "Debian GNU/Linux 7 (wheezy)",
ContainerRuntimeVersion: "docker://1.5.0", ContainerRuntimeVersion: "docker://1.10.0-rc1-fc24",
KubeletVersion: version.Get().String(), KubeletVersion: version.Get().String(),
KubeProxyVersion: version.Get().String(), KubeProxyVersion: version.Get().String(),
}, },
Capacity: api.ResourceList{ Capacity: api.ResourceList{
api.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI), api.ResourceCPU: *resource.NewMilliQuantity(2000, resource.DecimalSI),
api.ResourceMemory: *resource.NewQuantity(1024, resource.BinarySI), api.ResourceMemory: *resource.NewQuantity(20E9, resource.BinarySI),
api.ResourcePods: *resource.NewQuantity(0, resource.DecimalSI),
},
Allocatable: api.ResourceList{
api.ResourceCPU: *resource.NewMilliQuantity(1800, resource.DecimalSI),
api.ResourceMemory: *resource.NewQuantity(19900E6, resource.BinarySI),
api.ResourcePods: *resource.NewQuantity(0, resource.DecimalSI), api.ResourcePods: *resource.NewQuantity(0, resource.DecimalSI),
}, },
Addresses: []api.NodeAddress{ Addresses: []api.NodeAddress{
@ -2805,6 +2843,7 @@ func testDockerRuntimeVersion(t *testing.T) {
}, },
} }
kubelet.runtimeState = newRuntimeState(maxWaitForContainerRuntime, false, "", kubelet.isContainerRuntimeVersionCompatible)
kubelet.updateRuntimeUp() kubelet.updateRuntimeUp()
if err := kubelet.updateNodeStatus(); err != nil { if err := kubelet.updateNodeStatus(); err != nil {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)
@ -2836,13 +2875,14 @@ func testDockerRuntimeVersion(t *testing.T) {
t.Errorf("unexpected node condition order. NodeReady should be last.") t.Errorf("unexpected node condition order. NodeReady should be last.")
} }
if !reflect.DeepEqual(expectedNode, updatedNode) { if !api.Semantic.DeepEqual(expectedNode, updatedNode) {
t.Errorf("unexpected objects: %s", util.ObjectDiff(expectedNode, updatedNode)) t.Errorf("expected \n%v\n, got \n%v", expectedNode, updatedNode)
} }
// Downgrade docker version, node should be NotReady // Downgrade docker version, node should be NotReady
fakeRuntime.RuntimeType = "docker" fakeRuntime.RuntimeType = "docker"
fakeRuntime.VersionInfo = "1.17" fakeRuntime.VersionInfo = "1.5.0"
fakeRuntime.APIVersionInfo = "1.17"
kubelet.updateRuntimeUp() kubelet.updateRuntimeUp()
if err := kubelet.updateNodeStatus(); err != nil { if err := kubelet.updateNodeStatus(); err != nil {
t.Errorf("unexpected error: %v", err) t.Errorf("unexpected error: %v", err)
@ -2858,8 +2898,8 @@ func testDockerRuntimeVersion(t *testing.T) {
if !ok { if !ok {
t.Errorf("unexpected object type") t.Errorf("unexpected object type")
} }
if updatedNode.Status.Conditions[0].Reason != "KubeletNotReady" && if updatedNode.Status.Conditions[1].Reason != "KubeletNotReady" &&
!strings.Contains(updatedNode.Status.Conditions[0].Message, "container runtime version is older than") { !strings.Contains(updatedNode.Status.Conditions[1].Message, "container runtime version is older than") {
t.Errorf("unexpect NodeStatus due to container runtime version") t.Errorf("unexpect NodeStatus due to container runtime version")
} }
} }