rkt: Refactor version check with api-service.

Also mocked systemd interfaces for testing purpose.
This commit is contained in:
Yifan Gu 2015-11-18 18:35:31 -08:00
parent 8a24d831ec
commit 5b423dd458
5 changed files with 489 additions and 129 deletions

View File

@ -0,0 +1,123 @@
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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 rkt
import (
"fmt"
"strconv"
"sync"
"github.com/coreos/go-systemd/dbus"
rktapi "github.com/coreos/rkt/api/v1alpha"
"golang.org/x/net/context"
"google.golang.org/grpc"
)
// fakeRktInterface mocks the rktapi.PublicAPIClient interface for testing purpose.
type fakeRktInterface struct {
sync.Mutex
info rktapi.Info
called []string
err error
}
func newFakeRktInterface() *fakeRktInterface {
return &fakeRktInterface{}
}
func (f *fakeRktInterface) CleanCalls() {
f.Lock()
defer f.Unlock()
f.called = nil
}
func (f *fakeRktInterface) GetInfo(ctx context.Context, in *rktapi.GetInfoRequest, opts ...grpc.CallOption) (*rktapi.GetInfoResponse, error) {
f.Lock()
defer f.Unlock()
f.called = append(f.called, "GetInfo")
return &rktapi.GetInfoResponse{&f.info}, f.err
}
func (f *fakeRktInterface) ListPods(ctx context.Context, in *rktapi.ListPodsRequest, opts ...grpc.CallOption) (*rktapi.ListPodsResponse, error) {
return nil, fmt.Errorf("Not implemented")
}
func (f *fakeRktInterface) InspectPod(ctx context.Context, in *rktapi.InspectPodRequest, opts ...grpc.CallOption) (*rktapi.InspectPodResponse, error) {
return nil, fmt.Errorf("Not implemented")
}
func (f *fakeRktInterface) ListImages(ctx context.Context, in *rktapi.ListImagesRequest, opts ...grpc.CallOption) (*rktapi.ListImagesResponse, error) {
return nil, fmt.Errorf("Not implemented")
}
func (f *fakeRktInterface) InspectImage(ctx context.Context, in *rktapi.InspectImageRequest, opts ...grpc.CallOption) (*rktapi.InspectImageResponse, error) {
return nil, fmt.Errorf("Not implemented")
}
func (f *fakeRktInterface) ListenEvents(ctx context.Context, in *rktapi.ListenEventsRequest, opts ...grpc.CallOption) (rktapi.PublicAPI_ListenEventsClient, error) {
return nil, fmt.Errorf("Not implemented")
}
func (f *fakeRktInterface) GetLogs(ctx context.Context, in *rktapi.GetLogsRequest, opts ...grpc.CallOption) (rktapi.PublicAPI_GetLogsClient, error) {
return nil, fmt.Errorf("Not implemented")
}
// fakeSystemd mocks the systemdInterface for testing purpose.
// TODO(yifan): Remove this once we have a package for launching rkt pods.
// See https://github.com/coreos/rkt/issues/1769.
type fakeSystemd struct {
sync.Mutex
called []string
version string
err error
}
func newFakeSystemd() *fakeSystemd {
return &fakeSystemd{}
}
func (f *fakeSystemd) CleanCalls() {
f.Lock()
defer f.Unlock()
f.called = nil
}
func (f *fakeSystemd) Version() (systemdVersion, error) {
f.Lock()
defer f.Unlock()
f.called = append(f.called, "Version")
v, _ := strconv.Atoi(f.version)
return systemdVersion(v), f.err
}
func (f *fakeSystemd) ListUnits() ([]dbus.UnitStatus, error) {
return nil, fmt.Errorf("Not implemented")
}
func (f *fakeSystemd) StopUnit(name, mode string) (string, error) {
return "", fmt.Errorf("Not implemented")
}
func (f *fakeSystemd) RestartUnit(name, mode string) (string, error) {
return "", fmt.Errorf("Not implemented")
}
func (f *fakeSystemd) Reload() error {
return fmt.Errorf("Not implemented")
}

View File

@ -30,10 +30,12 @@ import (
"syscall"
"time"
"google.golang.org/grpc"
appcschema "github.com/appc/spec/schema"
appctypes "github.com/appc/spec/schema/types"
"github.com/coreos/go-systemd/dbus"
"github.com/coreos/go-systemd/unit"
rktapi "github.com/coreos/rkt/api/v1alpha"
"github.com/docker/docker/pkg/parsers"
docker "github.com/fsouza/go-dockerclient"
"github.com/golang/glog"
@ -53,10 +55,11 @@ import (
const (
RktType = "rkt"
acVersion = "0.7.1"
minimumRktVersion = "0.9.0"
recommendRktVersion = "0.9.0"
systemdMinimumVersion = "219"
minimumAppcVersion = "0.7.1"
minimumRktBinVersion = "0.9.0"
recommendedRktBinVersion = "0.9.0"
minimumRktApiVersion = "1.0.0-alpha"
minimumSystemdVersion = "219"
systemdServiceDir = "/run/systemd/system"
rktDataDir = "/var/lib/rkt"
@ -73,14 +76,18 @@ const (
authDir = "auth.d"
dockerAuthTemplate = `{"rktKind":"dockerAuth","rktVersion":"v1","registries":[%q],"credentials":{"user":%q,"password":%q}}`
defaultImageTag = "latest"
defaultImageTag = "latest"
defaultRktAPIServiceAddr = "localhost:15441"
)
// Runtime implements the Containerruntime for rkt. The implementation
// uses systemd, so in order to run this runtime, systemd must be installed
// on the machine.
type Runtime struct {
systemd *dbus.Conn
systemd systemdInterface
// The grpc client for rkt api-service.
apisvcConn *grpc.ClientConn
apisvc rktapi.PublicAPIClient
// The absolute path to rkt binary.
rktBinAbsPath string
config *Config
@ -93,6 +100,12 @@ type Runtime struct {
livenessManager proberesults.Manager
volumeGetter volumeGetter
imagePuller kubecontainer.ImagePuller
// Versions
binVersion rktVersion
apiVersion rktVersion
appcVersion rktVersion
systemdVersion systemdVersion
}
var _ kubecontainer.Runtime = &Runtime{}
@ -114,21 +127,16 @@ func New(config *Config,
imageBackOff *util.Backoff,
serializeImagePulls bool,
) (*Runtime, error) {
systemdVersion, err := getSystemdVersion()
// Create dbus connection.
systemd, err := newSystemd()
if err != nil {
return nil, err
}
result, err := systemdVersion.Compare(systemdMinimumVersion)
if err != nil {
return nil, err
}
if result < 0 {
return nil, fmt.Errorf("rkt: systemd version is too old, requires at least %v", systemdMinimumVersion)
return nil, fmt.Errorf("rkt: cannot create systemd interface: %v", err)
}
systemd, err := dbus.New()
// TODO(yifan): Use secure connection.
apisvcConn, err := grpc.Dial(defaultRktAPIServiceAddr, grpc.WithInsecure())
if err != nil {
return nil, fmt.Errorf("cannot connect to dbus: %v", err)
return nil, fmt.Errorf("rkt: cannot connect to rkt api service: %v", err)
}
rktBinAbsPath := config.Path
@ -144,6 +152,8 @@ func New(config *Config,
rkt := &Runtime{
systemd: systemd,
rktBinAbsPath: rktBinAbsPath,
apisvcConn: apisvcConn,
apisvc: rktapi.NewPublicAPIClient(apisvcConn),
config: config,
dockerKeyring: credentialprovider.NewDockerKeyring(),
containerRefManager: containerRefManager,
@ -158,28 +168,13 @@ func New(config *Config,
rkt.imagePuller = kubecontainer.NewImagePuller(recorder, rkt, imageBackOff)
}
// Test the rkt version.
version, err := rkt.Version()
if err != nil {
if err := rkt.checkVersion(minimumRktBinVersion, recommendedRktBinVersion, minimumAppcVersion, minimumRktApiVersion, minimumSystemdVersion); err != nil {
// TODO(yifan): Latest go-systemd version have the ability to close the
// dbus connection. However the 'docker/libcontainer' package is using
// the older go-systemd version, so we can't update the go-systemd version.
rkt.apisvcConn.Close()
return nil, err
}
result, err = version.Compare(minimumRktVersion)
if err != nil {
return nil, err
}
if result < 0 {
return nil, fmt.Errorf("rkt: version is too old, requires at least %v", minimumRktVersion)
}
result, err = version.Compare(recommendRktVersion)
if err != nil {
return nil, err
}
if result != 0 {
// TODO(yifan): Record an event to expose the information.
glog.Warningf("rkt: current version %q is not recommended (recommended version %q)", version, recommendRktVersion)
}
return rkt, nil
}
@ -880,33 +875,8 @@ func (r *Runtime) Type() string {
return RktType
}
// Version invokes 'rkt version' to get the version information of the rkt
// runtime on the machine.
// The return values are an int array containers the version number.
//
// Example:
// rkt:0.3.2+git --> []int{0, 3, 2}.
//
func (r *Runtime) Version() (kubecontainer.Version, error) {
output, err := r.runCommand("version")
if err != nil {
return nil, err
}
// Example output for 'rkt version':
// rkt version 0.3.2+git
// appc version 0.3.0+git
for _, line := range output {
tuples := strings.Split(strings.TrimSpace(line), " ")
if len(tuples) != 3 {
glog.Warningf("rkt: cannot parse the output: %q.", line)
continue
}
if tuples[0] == "rkt" {
return parseVersion(tuples[2])
}
}
return nil, fmt.Errorf("rkt: cannot determine the version")
return r.binVersion, nil
}
// TODO(yifan): This is very racy, unefficient, and unsafe, we need to provide

146
pkg/kubelet/rkt/rkt_test.go Normal file
View File

@ -0,0 +1,146 @@
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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 rkt
import (
"fmt"
"testing"
rktapi "github.com/coreos/rkt/api/v1alpha"
"github.com/stretchr/testify/assert"
)
func TestCheckVersion(t *testing.T) {
fr := newFakeRktInterface()
fs := newFakeSystemd()
r := &Runtime{apisvc: fr, systemd: fs}
fr.info = rktapi.Info{
RktVersion: "1.2.3+git",
AppcVersion: "1.2.4+git",
ApiVersion: "1.2.6-alpha",
}
fs.version = "100"
tests := []struct {
minimumRktBinVersion string
recommendedRktBinVersion string
minimumAppcVersion string
minimumRktApiVersion string
minimumSystemdVersion string
err error
calledGetInfo bool
calledSystemVersion bool
}{
// Good versions.
{
"1.2.3",
"1.2.3",
"1.2.4",
"1.2.5",
"99",
nil,
true,
true,
},
// Good versions.
{
"1.2.3+git",
"1.2.3+git",
"1.2.4+git",
"1.2.6-alpha",
"100",
nil,
true,
true,
},
// Requires greater binary version.
{
"1.2.4",
"1.2.4",
"1.2.4",
"1.2.6-alpha",
"100",
fmt.Errorf("rkt: binary version is too old(%v), requires at least %v", fr.info.RktVersion, "1.2.4"),
true,
true,
},
// Requires greater Appc version.
{
"1.2.3",
"1.2.3",
"1.2.5",
"1.2.6-alpha",
"100",
fmt.Errorf("rkt: appc version is too old(%v), requires at least %v", fr.info.AppcVersion, "1.2.5"),
true,
true,
},
// Requires greater API version.
{
"1.2.3",
"1.2.3",
"1.2.4",
"1.2.6",
"100",
fmt.Errorf("rkt: API version is too old(%v), requires at least %v", fr.info.ApiVersion, "1.2.6"),
true,
true,
},
// Requires greater API version.
{
"1.2.3",
"1.2.3",
"1.2.4",
"1.2.7",
"100",
fmt.Errorf("rkt: API version is too old(%v), requires at least %v", fr.info.ApiVersion, "1.2.7"),
true,
true,
},
// Requires greater systemd version.
{
"1.2.3",
"1.2.3",
"1.2.4",
"1.2.7",
"101",
fmt.Errorf("rkt: systemd version(%v) is too old, requires at least %v", fs.version, "101"),
false,
true,
},
}
for i, tt := range tests {
testCaseHint := fmt.Sprintf("test case #%d", i)
err := r.checkVersion(tt.minimumRktBinVersion, tt.recommendedRktBinVersion, tt.minimumAppcVersion, tt.minimumRktApiVersion, tt.minimumSystemdVersion)
assert.Equal(t, err, tt.err, testCaseHint)
if tt.calledGetInfo {
assert.Equal(t, fr.called, []string{"GetInfo"}, testCaseHint)
}
if tt.calledSystemVersion {
assert.Equal(t, fs.called, []string{"Version"}, testCaseHint)
}
if err == nil {
assert.Equal(t, r.binVersion.String(), fr.info.RktVersion, testCaseHint)
assert.Equal(t, r.appcVersion.String(), fr.info.AppcVersion, testCaseHint)
assert.Equal(t, r.apiVersion.String(), fr.info.ApiVersion, testCaseHint)
}
fr.CleanCalls()
fs.CleanCalls()
}
}

103
pkg/kubelet/rkt/systemd.go Normal file
View File

@ -0,0 +1,103 @@
/*
Copyright 2015 The Kubernetes Authors All rights reserved.
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 rkt
import (
"fmt"
"os/exec"
"strconv"
"strings"
"github.com/coreos/go-systemd/dbus"
)
// systemdVersion is a type wraps the int to implement kubecontainer.Version interface.
type systemdVersion int
func (s systemdVersion) String() string {
return fmt.Sprintf("%d", s)
}
func (s systemdVersion) Compare(other string) (int, error) {
v, err := strconv.Atoi(other)
if err != nil {
return -1, err
}
if int(s) < v {
return -1, nil
} else if int(s) > v {
return 1, nil
}
return 0, nil
}
// systemdInterface is an abstraction of the go-systemd/dbus to make
// it mockable for testing.
// TODO(yifan): Eventually we should move these functionalities to:
// 1. a package for launching/stopping rkt pods.
// 2. rkt api-service interface for listing pods.
// See https://github.com/coreos/rkt/issues/1769.
type systemdInterface interface {
// Version returns the version of the systemd.
Version() (systemdVersion, error)
// ListUnits lists all the loaded units.
ListUnits() ([]dbus.UnitStatus, error)
// StopUnits stops the unit with the given name.
StopUnit(name, mode string) (string, error)
// StopUnits restarts the unit with the given name.
RestartUnit(name, mode string) (string, error)
// Reload is equivalent to 'systemctl daemon-reload'.
Reload() error
}
// systemd implements the systemdInterface using dbus and systemctl.
// All the functions other then Version() are already implemented by go-systemd/dbus.
type systemd struct {
*dbus.Conn
}
// newSystemd creates a systemd object that implements systemdInterface.
func newSystemd() (*systemd, error) {
dbusConn, err := dbus.New()
if err != nil {
return nil, err
}
return &systemd{dbusConn}, nil
}
// Version returns the version of the systemd.
func (s *systemd) Version() (systemdVersion, error) {
output, err := exec.Command("systemctl", "--version").Output()
if err != nil {
return -1, err
}
// Example output of 'systemctl --version':
//
// systemd 215
// +PAM +AUDIT +SELINUX +IMA +SYSVINIT +LIBCRYPTSETUP +GCRYPT +ACL +XZ -SECCOMP -APPARMOR
//
lines := strings.Split(string(output), "\n")
tuples := strings.Split(lines[0], " ")
if len(tuples) != 2 {
return -1, fmt.Errorf("rkt: Failed to parse version %v", lines)
}
result, err := strconv.Atoi(string(tuples[1]))
if err != nil {
return -1, err
}
return systemdVersion(result), nil
}

View File

@ -18,93 +18,111 @@ package rkt
import (
"fmt"
"os/exec"
"strconv"
"strings"
appctypes "github.com/appc/spec/schema/types"
"github.com/coreos/go-semver/semver"
rktapi "github.com/coreos/rkt/api/v1alpha"
"github.com/golang/glog"
"golang.org/x/net/context"
)
type rktVersion []int
// rktVersion implementes kubecontainer.Version interface by implementing
// Compare() and String() (which is implemented by the underlying semver.Version)
type rktVersion struct {
*semver.Version
}
func parseVersion(input string) (rktVersion, error) {
nsv, err := appctypes.NewSemVer(input)
func newRktVersion(version string) (rktVersion, error) {
sem, err := semver.NewVersion(version)
if err != nil {
return nil, err
return rktVersion{}, err
}
return rktVersion{int(nsv.Major), int(nsv.Minor), int(nsv.Patch)}, nil
return rktVersion{sem}, nil
}
func (r rktVersion) Compare(other string) (int, error) {
v, err := parseVersion(other)
v, err := semver.NewVersion(other)
if err != nil {
return -1, err
}
for i := range r {
if i > len(v)-1 {
return 1, nil
}
if r[i] < v[i] {
return -1, nil
}
if r[i] > v[i] {
return 1, nil
}
}
// When loop ends, len(r) is <= len(v).
if len(r) < len(v) {
if r.LessThan(*v) {
return -1, nil
}
return 0, nil
}
func (r rktVersion) String() string {
var version []string
for _, v := range r {
version = append(version, fmt.Sprintf("%d", v))
}
return strings.Join(version, ".")
}
type systemdVersion int
func (s systemdVersion) String() string {
return fmt.Sprintf("%d", s)
}
func (s systemdVersion) Compare(other string) (int, error) {
v, err := strconv.Atoi(other)
if err != nil {
return -1, err
}
if int(s) < v {
return -1, nil
} else if int(s) > v {
if v.LessThan(*r.Version) {
return 1, nil
}
return 0, nil
}
func getSystemdVersion() (systemdVersion, error) {
output, err := exec.Command("systemctl", "--version").Output()
// checkVersion tests whether the rkt/systemd/rkt-api-service that meet the version requirement.
// If all version requirements are met, it returns nil.
func (r *Runtime) checkVersion(minimumRktBinVersion, recommendedRktBinVersion, minimumAppcVersion, minimumRktApiVersion, minimumSystemdVersion string) error {
// Check systemd version.
var err error
r.systemdVersion, err = r.systemd.Version()
if err != nil {
return -1, err
return err
}
// Example output of 'systemctl --version':
//
// systemd 215
// +PAM +AUDIT +SELINUX +IMA +SYSVINIT +LIBCRYPTSETUP +GCRYPT +ACL +XZ -SECCOMP -APPARMOR
//
lines := strings.Split(string(output), "\n")
tuples := strings.Split(lines[0], " ")
if len(tuples) != 2 {
return -1, fmt.Errorf("rkt: Failed to parse version %v", lines)
}
result, err := strconv.Atoi(string(tuples[1]))
result, err := r.systemdVersion.Compare(minimumSystemdVersion)
if err != nil {
return -1, err
return err
}
return systemdVersion(result), nil
if result < 0 {
return fmt.Errorf("rkt: systemd version(%v) is too old, requires at least %v", r.systemdVersion, minimumSystemdVersion)
}
// Example for the version strings returned by GetInfo():
// RktVersion:"0.10.0+gitb7349b1" AppcVersion:"0.7.1" ApiVersion:"1.0.0-alpha"
resp, err := r.apisvc.GetInfo(context.Background(), &rktapi.GetInfoRequest{})
if err != nil {
return err
}
// Check rkt binary version.
r.binVersion, err = newRktVersion(resp.Info.RktVersion)
if err != nil {
return err
}
result, err = r.binVersion.Compare(minimumRktBinVersion)
if err != nil {
return err
}
if result < 0 {
return fmt.Errorf("rkt: binary version is too old(%v), requires at least %v", resp.Info.RktVersion, minimumRktBinVersion)
}
result, err = r.binVersion.Compare(recommendedRktBinVersion)
if err != nil {
return err
}
if result != 0 {
// TODO(yifan): Record an event to expose the information.
glog.Warningf("rkt: current binary version %q is not recommended (recommended version %q)", resp.Info.RktVersion, recommendedRktBinVersion)
}
// Check Appc version.
r.appcVersion, err = newRktVersion(resp.Info.AppcVersion)
if err != nil {
return err
}
result, err = r.appcVersion.Compare(minimumAppcVersion)
if err != nil {
return err
}
if result < 0 {
return fmt.Errorf("rkt: appc version is too old(%v), requires at least %v", resp.Info.AppcVersion, minimumAppcVersion)
}
// Check rkt API version.
r.apiVersion, err = newRktVersion(resp.Info.ApiVersion)
if err != nil {
return err
}
result, err = r.apiVersion.Compare(minimumRktApiVersion)
if err != nil {
return err
}
if result < 0 {
return fmt.Errorf("rkt: API version is too old(%v), requires at least %v", resp.Info.ApiVersion, minimumRktApiVersion)
}
return nil
}