diff --git a/glide.lock b/glide.lock index aeb4b61ac..0727bf777 100644 --- a/glide.lock +++ b/glide.lock @@ -1,5 +1,5 @@ -hash: b3a6656f552e85def420fa859d14f27dc2897209ce315ed25ce9cadeff6fae21 -updated: 2016-07-20T00:33:41.257155246-07:00 +hash: 9943a35009296355c6a66485228855db99eefab65890c8fa89a4776cba9c9b71 +updated: 2016-07-28T17:22:20.295749043-07:00 imports: - name: github.com/beorn7/perks version: 3ac7bf7a47d159a033b107610db8a1b6575507a4 @@ -173,6 +173,10 @@ imports: - libcontainer/utils - name: github.com/pborman/uuid version: ca53cad383cad2479bbba7f7a1a05797ec1386e4 +- name: github.com/pmezard/go-difflib + version: d8ed2627bdf02c080bf22230dbb337003b7aba2d + subpackages: + - difflib - name: github.com/prometheus/client_golang version: 3b78d7a77f51ccbc364d4bc170920153022cfd08 subpackages: @@ -189,13 +193,28 @@ imports: - model - name: github.com/prometheus/procfs version: 490cc6eb5fa45bf8a8b7b73c8bc82a8160e8531d +- name: github.com/smartystreets/goconvey + version: d4c757aa9afd1e2fc1832aaab209b5794eb336e1 + subpackages: + - convey + - convey/reporting + - convey/gotest - name: github.com/spf13/pflag version: 08b1a584251b5b62f458943640fc8ebd4d50aaa5 +- name: github.com/stretchr/objx + version: 1a9d0bb9f541897e62256577b352fdbc1fb4fd94 +- name: github.com/stretchr/testify + version: f390dcf405f7b83c997eac1b06768bb9f44dec18 + subpackages: + - mock + - assert - name: github.com/ugorji/go version: f4485b318aadd133842532f841dc205a8e339d74 subpackages: - codec - codec/codecgen +- name: github.com/vektra/errors + version: c64d83aba85aa4392895aadeefabbd24e89f3580 - name: golang.org/x/net version: 62685c2d7ca23c807425dca88b11a3e2323dab41 subpackages: @@ -333,4 +352,15 @@ imports: - pkg/kubelet/qos/util - pkg/util/hash - pkg/util/net/sets -testImports: [] +testImports: +- name: github.com/gopherjs/gopherjs + version: 72e303cb5f235b23471872477b57e573dc1c71d0 + subpackages: + - js +- name: github.com/jtolds/gls + version: 8ddce2a84170772b95dd5d576c48d517b22cac63 +- name: github.com/smartystreets/assertions + version: 2063fd1cc7c975db70502811a34b06ad034ccdf2 + subpackages: + - internal/go-render/render + - internal/oglematchers diff --git a/glide.yaml b/glide.yaml index 99ace9991..d8a9770f1 100644 --- a/glide.yaml +++ b/glide.yaml @@ -9,3 +9,12 @@ import: subpackages: - pkg/api - pkg/client/unversioned +- package: github.com/smartystreets/goconvey + version: 1.6.2 + subpackages: + - convey +- package: github.com/stretchr/testify + version: v1.1.3 + subpackages: + - mock +- package: github.com/vektra/errors diff --git a/main.go b/main.go index 3162dd03d..fb29662a8 100644 --- a/main.go +++ b/main.go @@ -47,6 +47,23 @@ var ( // Labels are a Kubernetes representation of discovered features. type Labels map[string]string +// APIHelpers represents a set of API helpers for Kubernetes +type APIHelpers interface { + // GetClient returns a client + GetClient() (*client.Client, error) + + // GetNode returns the Kubernetes node on which this container is running. + GetNode(*client.Client) (*api.Node, error) + + // addLabels modifies the supplied node's labels collection. + // In order to publish the labels, the node must be subsequently updated via the + // API server using the client library. + AddLabels(*api.Node, Labels) + + // UpdateNode updates the node via the API server using a client. + UpdateNode(*client.Client, *api.Node) error +} + func main() { // Assert that the version is known if version == "" { @@ -109,7 +126,12 @@ func main() { // Do feature discovery from all configured sources. for _, source := range sources { - for name, value := range featureLabels(source) { + labelsFromSource, err := getFeatureLabels(source) + if err != nil { + log.Fatalf("discovery failed for source [%s]: %s", source.Name(), err.Error()) + } + + for name, value := range labelsFromSource { labels[name] = value // Log discovered feature. log.Printf("%s = %s", name, value) @@ -118,50 +140,72 @@ func main() { // Update the node with the node labels, unless disabled via flags. if !noPublish { - // Set up K8S client. - cli, err := client.NewInCluster() + helper := APIHelpers(k8sHelpers{}) + err := advertiseFeatureLabels(helper, labels) if err != nil { - log.Fatalf("can't get kubernetes client: %s", err.Error()) - } - - // Get the current node. - node := getNode(cli) - // Add labels to the node object. - addLabels(node, labels) - // Send the updated node to the apiserver. - _, err = cli.Nodes().Update(node) - if err != nil { - log.Fatalf("can't update node: %s", err.Error()) + log.Fatalf("failed to advertise labels: %s", err.Error()) } } } -// featureLabels returns node labels for features discovered by the +// getFeatureLabels returns node labels for features discovered by the // supplied source. -func featureLabels(source FeatureSource) Labels { +func getFeatureLabels(source FeatureSource) (Labels, error) { labels := Labels{} features, err := source.Discover() if err != nil { - log.Fatalf("discovery failed for source [%s]: %s", source.Name(), err.Error()) + return nil, err } for _, f := range features { labels[fmt.Sprintf("%s-%s-%s", prefix, source.Name(), f)] = "true" } - return labels + return labels, nil } -// addLabels modifies the supplied node's labels collection. -// -// In order to publish the labels, the node must be subsequently updated via the -// API server using the client library. -func addLabels(n *api.Node, labels Labels) { - for k, v := range labels { - n.Labels[k] = v +// advertiseFeatureLabels advertises the feature labels to a Kubernetes node +// via the API server. +func advertiseFeatureLabels(helper APIHelpers, labels Labels) error { + // Set up K8S client. + cli, err := helper.GetClient() + if err != nil { + log.Printf("can't get kubernetes client: %s", err.Error()) + return err } + + // Get the current node. + node, err := helper.GetNode(cli) + if err != nil { + log.Printf("failed to get node: %s", err.Error()) + return err + } + + // Add labels to the node object. + helper.AddLabels(node, labels) + + // Send the updated node to the apiserver. + err = helper.UpdateNode(cli, node) + if err != nil { + log.Printf("can't update node: %s", err.Error()) + return err + } + + return nil } -// getNode returns the Kubernetes node on which this container is running. -func getNode(cli *client.Client) *api.Node { +// Implements main.APIHelpers +type k8sHelpers struct{} + +func (h k8sHelpers) GetClient() (*client.Client, error) { + // Set up K8S client. + cli, err := client.NewInCluster() + if err != nil { + return nil, err + } + + return cli, nil +} + +func (h k8sHelpers) GetNode(cli *client.Client) (*api.Node, error) { // Get the pod name and pod namespace from the env variables podName := os.Getenv(PodNameEnv) podns := os.Getenv(PodNamespaceEnv) @@ -171,14 +215,32 @@ func getNode(cli *client.Client) *api.Node { // Get the pod object using the pod name and pod namespace pod, err := cli.Pods(podns).Get(podName) if err != nil { - log.Fatalf("can't get pod: %s", err.Error()) + log.Printf("can't get pods: %s", err.Error()) + return nil, err } // Get the node object using the pod name and pod namespace node, err := cli.Nodes().Get(pod.Spec.NodeName) if err != nil { - log.Fatalf("can't get node: %s", err.Error()) + log.Printf("can't get node: %s", err.Error()) + return nil, err } - return node + return node, nil +} + +func (h k8sHelpers) AddLabels(n *api.Node, labels Labels) { + for k, v := range labels { + n.Labels[k] = v + } +} + +func (h k8sHelpers) UpdateNode(c *client.Client, n *api.Node) error { + // Send the updated node to the apiserver. + _, err := c.Nodes().Update(n) + if err != nil { + return err + } + + return nil } diff --git a/main_test.go b/main_test.go new file mode 100644 index 000000000..08cee635a --- /dev/null +++ b/main_test.go @@ -0,0 +1,103 @@ +package main + +import ( + "fmt" + "testing" + + // "github.com/intelsdi-x/dbi-iafeature-discovery/mocks" + . "github.com/smartystreets/goconvey/convey" + "github.com/vektra/errors" + "k8s.io/kubernetes/pkg/api" + client "k8s.io/kubernetes/pkg/client/unversioned" +) + +func TestDiscoveryWithMockSources(t *testing.T) { + Convey("When I discover features from fake source and update the node using fake client", t, func() { + mockFeatureSource := new(MockFeatureSource) + fakeFeatureSourceName := string("testSource") + fakeFeatures := []string{"testfeature1", "testfeature2", "testfeature3"} + fakeFeatureLabels := Labels{} + for _, f := range fakeFeatures { + fakeFeatureLabels[fmt.Sprintf("%s-testSource-%s", prefix, f)] = "true" + } + fakeFeatureSource := FeatureSource(mockFeatureSource) + + Convey("When I successfully get the labels from the mock source", func() { + mockFeatureSource.On("Name").Return(fakeFeatureSourceName) + mockFeatureSource.On("Discover").Return(fakeFeatures, nil) + + returnedLabels, err := getFeatureLabels(fakeFeatureSource) + Convey("Proper label is returned", func() { + So(returnedLabels, ShouldResemble, fakeFeatureLabels) + }) + Convey("Error is nil", func() { + So(err, ShouldBeNil) + }) + }) + + Convey("When I fail to get the labels from the mock source", func() { + expectedError := errors.New("fake error") + mockFeatureSource.On("Discover").Return(nil, expectedError) + + returnedLabels, err := getFeatureLabels(fakeFeatureSource) + Convey("No label is returned", func() { + So(returnedLabels, ShouldBeNil) + }) + Convey("Error is produced", func() { + So(err, ShouldEqual, expectedError) + }) + }) + + mockAPIHelper := new(MockAPIHelpers) + testHelper := APIHelpers(mockAPIHelper) + var mockClient *client.Client + var mockNode *api.Node + + Convey("When I successfully advertise feature labels to a node", func() { + mockAPIHelper.On("GetClient").Return(mockClient, nil) + mockAPIHelper.On("GetNode", mockClient).Return(mockNode, nil).Once() + mockAPIHelper.On("AddLabels", mockNode, fakeFeatureLabels).Return().Once() + mockAPIHelper.On("UpdateNode", mockClient, mockNode).Return(nil).Once() + err := advertiseFeatureLabels(testHelper, fakeFeatureLabels) + + Convey("Error is nil", func() { + So(err, ShouldBeNil) + }) + }) + + Convey("When I fail to get a mock client while advertising feature labels", func() { + expectedError := errors.New("fake error") + mockAPIHelper.On("GetClient").Return(nil, expectedError) + err := advertiseFeatureLabels(testHelper, fakeFeatureLabels) + + Convey("Error is produced", func() { + So(err, ShouldEqual, expectedError) + }) + }) + + Convey("When I fail to get a mock node while advertising feature labels", func() { + expectedError := errors.New("fake error") + mockAPIHelper.On("GetClient").Return(mockClient, nil) + mockAPIHelper.On("GetNode", mockClient).Return(nil, expectedError).Once() + err := advertiseFeatureLabels(testHelper, fakeFeatureLabels) + + Convey("Error is produced", func() { + So(err, ShouldEqual, expectedError) + }) + }) + + Convey("When I fail to update a mock node while advertising feature labels", func() { + expectedError := errors.New("fake error") + mockAPIHelper.On("GetClient").Return(mockClient, nil) + mockAPIHelper.On("GetNode", mockClient).Return(mockNode, nil).Once() + mockAPIHelper.On("AddLabels", mockNode, fakeFeatureLabels).Return().Once() + mockAPIHelper.On("UpdateNode", mockClient, mockNode).Return(expectedError).Once() + err := advertiseFeatureLabels(testHelper, fakeFeatureLabels) + + Convey("Error is produced", func() { + So(err, ShouldEqual, expectedError) + }) + }) + + }) +} diff --git a/mockapihelpers.go b/mockapihelpers.go new file mode 100644 index 000000000..ed577aa37 --- /dev/null +++ b/mockapihelpers.go @@ -0,0 +1,80 @@ +package main + +import ( + "github.com/stretchr/testify/mock" + "k8s.io/kubernetes/pkg/api" + client "k8s.io/kubernetes/pkg/client/unversioned" +) + +type MockAPIHelpers struct { + mock.Mock +} + +// GetClient provides a mock function with no input arguments and +// *client.Client and error as return value +func (_m *MockAPIHelpers) GetClient() (*client.Client, error) { + ret := _m.Called() + + var r0 *client.Client + if rf, ok := ret.Get(0).(func() *client.Client); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*client.Client) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetNode provides a mock function with *client.Client as input argument and +// *api.Node and error as return values +func (_m *MockAPIHelpers) GetNode(_a0 *client.Client) (*api.Node, error) { + ret := _m.Called(_a0) + + var r0 *api.Node + if rf, ok := ret.Get(0).(func(*client.Client) *api.Node); ok { + r0 = rf(_a0) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*api.Node) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(*client.Client) error); ok { + r1 = rf(_a0) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// AddLabels provides a mock function with *api.Node and main.Labels as the input arguments and +// no return value +func (_m *MockAPIHelpers) AddLabels(_a0 *api.Node, _a1 Labels) { + _m.Called(_a0, _a1) +} + +// UpdateNode provides a mock function with *client.Client and *api.Node as the input arguments and +// error as the return value +func (_m *MockAPIHelpers) UpdateNode(_a0 *client.Client, _a1 *api.Node) error { + ret := _m.Called(_a0, _a1) + + var r0 error + if rf, ok := ret.Get(0).(func(*client.Client, *api.Node) error); ok { + r0 = rf(_a0, _a1) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/mockfeaturesource.go b/mockfeaturesource.go new file mode 100644 index 000000000..765266398 --- /dev/null +++ b/mockfeaturesource.go @@ -0,0 +1,46 @@ +package main + +import "github.com/stretchr/testify/mock" + +type MockFeatureSource struct { + mock.Mock +} + +// Name provides a mock function with no input arguments +// and string as return value +func (_m *MockFeatureSource) Name() string { + ret := _m.Called() + + var r0 string + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + return r0 +} + +// Discover provides a mock function with no input arguments +// and []string and error as the return values +func (_m *MockFeatureSource) Discover() ([]string, error) { + ret := _m.Called() + + var r0 []string + if rf, ok := ret.Get(0).(func() []string); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/sources.go b/sources.go index c51426207..62221bd51 100644 --- a/sources.go +++ b/sources.go @@ -41,6 +41,7 @@ func (s cpuidSource) Discover() ([]string, error) { //////////////////////////////////////////////////////////////////////////////// // RDT (Intel Resource Director Technology) Source +// Implements main.FeatureSource. type rdtSource struct{} func (s rdtSource) Name() string { return "rdt" } @@ -51,7 +52,7 @@ func (s rdtSource) Discover() ([]string, error) { out, err := exec.Command("bash", "-c", path.Join(RDTBin, "mon-discovery")).Output() if err != nil { - return nil, fmt.Errorf("can't detect support for RDT monitoring: %v", err) + return nil, fmt.Errorf("can't detect support for RDT monitoring: %s", err.Error()) } if string(out[:]) == DETECTED { // RDT monitoring detected. @@ -60,7 +61,7 @@ func (s rdtSource) Discover() ([]string, error) { out, err = exec.Command("bash", "-c", path.Join(RDTBin, "l3-alloc-discovery")).Output() if err != nil { - return nil, fmt.Errorf("can't detect support for RDT L3 allocation: %v", err) + return nil, fmt.Errorf("can't detect support for RDT L3 allocation: %s", err.Error()) } if string(out[:]) == DETECTED { // RDT L3 cache allocation detected. @@ -69,7 +70,7 @@ func (s rdtSource) Discover() ([]string, error) { out, err = exec.Command("bash", "-c", path.Join(RDTBin, "l2-alloc-discovery")).Output() if err != nil { - return nil, fmt.Errorf("can't detect support for RDT L2 allocation: %v", err) + return nil, fmt.Errorf("can't detect support for RDT L2 allocation: %s", err.Error()) } if string(out[:]) == DETECTED { // RDT L2 cache allocation detected.