提交 5b450ece 编写于 作者: S Sebastian Florek

Add related pods to service detail page. Refactor heapster client to allow testing. (#739)

上级 597f6e47
......@@ -26,8 +26,13 @@ type HeapsterClient interface {
// Creates a new GET HTTP request to heapster, specified by the path param, to the V1 API
// endpoint. The path param is without the API prefix, e.g.,
// /model/namespaces/default/pod-list/foo/metrics/memory-usage
Get(path string) RequestInterface
}
Get(path string) *restclient.Request
// RequestInterface is an interface that allows to make operations on pure request object.
// Separation is done to allow testing.
type RequestInterface interface {
DoRaw() ([]byte, error)
}
// InClusterHeapsterClient is an in-cluster implementation of a Heapster client. Talks with Heapster
......@@ -37,7 +42,7 @@ type InClusterHeapsterClient struct {
}
// InClusterHeapsterClient.Get creates request to given path.
func (c InClusterHeapsterClient) Get(path string) *restclient.Request {
func (c InClusterHeapsterClient) Get(path string) RequestInterface {
return c.client.Get().Prefix("proxy").
Namespace("kube-system").
Resource("services").
......@@ -52,7 +57,7 @@ type RemoteHeapsterClient struct {
}
// RemoteHeapsterClient.Get creates request to given path.
func (c RemoteHeapsterClient) Get(path string) *restclient.Request {
func (c RemoteHeapsterClient) Get(path string) RequestInterface {
return c.client.Get().Suffix(path)
}
......
......@@ -300,7 +300,8 @@ func (apiHandler *ApiHandler) handleGetServiceList(request *restful.Request, res
func (apiHandler *ApiHandler) handleGetServiceDetail(request *restful.Request, response *restful.Response) {
namespace := request.PathParameter("namespace")
service := request.PathParameter("service")
result, err := resourceService.GetServiceDetail(apiHandler.client, namespace, service)
result, err := resourceService.GetServiceDetail(apiHandler.client, apiHandler.heapsterClient,
namespace, service)
if err != nil {
handleInternalError(response, err)
return
......
......@@ -18,9 +18,11 @@ import (
"log"
"k8s.io/kubernetes/pkg/api"
client "k8s.io/kubernetes/pkg/client/unversioned"
k8sClient "k8s.io/kubernetes/pkg/client/unversioned"
"github.com/kubernetes/dashboard/client"
"github.com/kubernetes/dashboard/resource/common"
"github.com/kubernetes/dashboard/resource/pod"
)
// Service is a representation of a service.
......@@ -45,10 +47,15 @@ type ServiceDetail struct {
// ClusterIP is usually assigned by the master. Valid values are None, empty string (""), or
// a valid IP address. None can be specified for headless services when proxying is not required
ClusterIP string `json:"clusterIP"`
// PodList represents list of pods targeted by same label selector as this service.
PodList pod.PodList `json:"podList"`
}
// GetServiceDetail gets service details.
func GetServiceDetail(client client.Interface, namespace, name string) (*ServiceDetail, error) {
func GetServiceDetail(client k8sClient.Interface, heapsterClient client.HeapsterClient,
namespace, name string) (*ServiceDetail, error) {
log.Printf("Getting details of %s service in %s namespace", name, namespace)
// TODO(maciaszczykm): Use channels.
......@@ -57,6 +64,32 @@ func GetServiceDetail(client client.Interface, namespace, name string) (*Service
return nil, err
}
podList, err := GetServicePods(client, heapsterClient, namespace, serviceData.Spec.Selector)
if err != nil {
return nil, err
}
service := ToServiceDetail(serviceData)
service.PodList = *podList
return &service, nil
}
// GetServicePods gets list of pods targeted by given label selector in given namespace.
func GetServicePods(client k8sClient.Interface, heapsterClient client.HeapsterClient,
namespace string, serviceSelector map[string]string) (*pod.PodList, error) {
channels := &common.ResourceChannels{
PodList: common.GetPodListChannel(client, 1),
}
apiPodList := <-channels.PodList.List
if err := <-channels.PodList.Error; err != nil {
return nil, err
}
apiPods := common.FilterNamespacedPodsBySelector(apiPodList.Items, namespace, serviceSelector)
podList := pod.CreatePodList(apiPods, heapsterClient)
return &podList, nil
}
......@@ -284,7 +284,8 @@ backendApi.Pod;
* externalEndpoints: !Array<!backendApi.Endpoint>,
* selector: !Object<string, string>,
* type: string,
* clusterIP: string
* clusterIP: string,
* podList: !backendApi.PodList
* }}
*/
backendApi.ServiceDetail;
......
......@@ -15,3 +15,10 @@ limitations under the License.
-->
<kd-service-info service="ctrl.serviceDetail"></kd-service-info>
<kd-content-card ng-if="ctrl.serviceDetail.podList.pods.length">
<kd-title>Pods</kd-title>
<kd-content>
<kd-pod-card-list pod-list="ctrl.serviceDetail.podList"></kd-pod-card-list>
</kd-content>
</kd-content-card>
......@@ -19,21 +19,22 @@ import (
"testing"
"k8s.io/kubernetes/pkg/api"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/apis/extensions"
"k8s.io/kubernetes/pkg/client/restclient"
k8sClient "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/client/unversioned/testclient"
"github.com/kubernetes/dashboard/client"
"github.com/kubernetes/dashboard/resource/common"
"github.com/kubernetes/dashboard/resource/pod"
"k8s.io/kubernetes/pkg/api/unversioned"
"k8s.io/kubernetes/pkg/client/restclient"
)
type FakeHeapsterClient struct {
client k8sClient.Interface
}
func (c FakeHeapsterClient) Get(path string) *restclient.Request {
func (c FakeHeapsterClient) Get(path string) client.RequestInterface {
return &restclient.Request{}
}
......
......@@ -19,11 +19,28 @@ import (
"testing"
"k8s.io/kubernetes/pkg/api"
k8sClient "k8s.io/kubernetes/pkg/client/unversioned"
"k8s.io/kubernetes/pkg/client/unversioned/testclient"
"github.com/kubernetes/dashboard/client"
"github.com/kubernetes/dashboard/resource/common"
"github.com/kubernetes/dashboard/resource/pod"
)
type FakeHeapsterClient struct {
client k8sClient.Interface
}
type FakeRequest struct{}
func (FakeRequest) DoRaw() ([]byte, error) {
return nil, nil
}
func (c FakeHeapsterClient) Get(path string) client.RequestInterface {
return FakeRequest{}
}
func TestGetServiceDetail(t *testing.T) {
cases := []struct {
service *api.Service
......@@ -33,17 +50,18 @@ func TestGetServiceDetail(t *testing.T) {
}{
{
service: &api.Service{},
namespace: "test-namespace", name: "test-name",
expectedActions: []string{"get"},
namespace: "test-namespace-1", name: "test-name",
expectedActions: []string{"get", "list"},
expected: &ServiceDetail{
TypeMeta: common.TypeMeta{Kind: common.ResourceKindService},
PodList: pod.PodList{Pods: []pod.Pod{}},
},
}, {
service: &api.Service{ObjectMeta: api.ObjectMeta{
Name: "test-service", Namespace: "test-namespace",
}},
namespace: "test-namespace", name: "test-name",
expectedActions: []string{"get"},
namespace: "test-namespace-2", name: "test-name",
expectedActions: []string{"get", "list"},
expected: &ServiceDetail{
ObjectMeta: common.ObjectMeta{
Name: "test-service",
......@@ -51,14 +69,16 @@ func TestGetServiceDetail(t *testing.T) {
},
TypeMeta: common.TypeMeta{Kind: common.ResourceKindService},
InternalEndpoint: common.Endpoint{Host: "test-service.test-namespace"},
PodList: pod.PodList{Pods: []pod.Pod{}},
},
},
}
for _, c := range cases {
fakeClient := testclient.NewSimpleFake(c.service)
fakeHeapsterClient := FakeHeapsterClient{client: testclient.NewSimpleFake()}
actual, _ := GetServiceDetail(fakeClient, c.namespace, c.name)
actual, _ := GetServiceDetail(fakeClient, fakeHeapsterClient, c.namespace, c.name)
actions := fakeClient.Actions()
if len(actions) != len(c.expectedActions) {
......@@ -80,3 +100,72 @@ func TestGetServiceDetail(t *testing.T) {
}
}
}
func TestGetServicePods(t *testing.T) {
firstSelector := map[string]string{"app": "selector-1"}
secondSelector := map[string]string{"app": "selector-2"}
cases := []struct {
namespace string
serviceSelector map[string]string
podList *api.PodList
expectedActions []string
expected *pod.PodList
}{
{
"test-namespace-1", firstSelector,
&api.PodList{Items: []api.Pod{}}, []string{"list"}, &pod.PodList{Pods: []pod.Pod{}},
}, {
"test-namespace-2",
firstSelector,
&api.PodList{Items: []api.Pod{{ObjectMeta: api.ObjectMeta{
Name: "test-pod",
Labels: secondSelector,
}}}},
[]string{"list"},
&pod.PodList{Pods: []pod.Pod{}},
}, {
"test-namespace-3",
firstSelector,
&api.PodList{Items: []api.Pod{{ObjectMeta: api.ObjectMeta{
Name: "test-pod",
Labels: firstSelector,
Namespace: "test-namespace-3",
}}}},
[]string{"list"},
&pod.PodList{Pods: []pod.Pod{{
ObjectMeta: common.ObjectMeta{
Name: "test-pod",
Labels: firstSelector,
Namespace: "test-namespace-3",
},
TypeMeta: common.TypeMeta{Kind: common.ResourceKindPod},
}}},
},
}
for _, c := range cases {
fakeClient := testclient.NewSimpleFake(c.podList)
fakeHeapsterClient := FakeHeapsterClient{client: testclient.NewSimpleFake()}
actual, _ := GetServicePods(fakeClient, fakeHeapsterClient, c.namespace, c.serviceSelector)
actions := fakeClient.Actions()
if len(actions) != len(c.expectedActions) {
t.Errorf("Unexpected actions: %v, expected %d actions got %d", actions,
len(c.expectedActions), len(actions))
continue
}
for i, verb := range c.expectedActions {
if actions[i].GetVerb() != verb {
t.Errorf("Unexpected action: %+v, expected %s",
actions[i], verb)
}
}
if !reflect.DeepEqual(actual, c.expected) {
t.Errorf("GetServicePods(client, heapsterClient, %#v, %#v) == \ngot %#v, \nexpected %#v",
c.namespace, c.serviceSelector, actual, c.expected)
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册