diff --git a/src/app/backend/handler/apihandler.go b/src/app/backend/handler/apihandler.go index dd2402f30a60..d4d11b6b10a0 100644 --- a/src/app/backend/handler/apihandler.go +++ b/src/app/backend/handler/apihandler.go @@ -175,6 +175,11 @@ func CreateHttpApiHandler(client *client.Client, heapsterClient HeapsterClient, To(apiHandler.handleGetDeployments). Writes(deployment.DeploymentList{})) + apiV1Ws.Route( + apiV1Ws.GET("/deployment/{namespace}/{deployment}"). + To(apiHandler.handleGetDeploymentDetail). + Writes(deployment.DeploymentDetail{})) + apiV1Ws.Route( apiV1Ws.GET("/daemonset"). To(apiHandler.handleGetDaemonSetList). @@ -420,6 +425,22 @@ func (apiHandler *ApiHandler) handleGetDeployments( response.WriteHeaderAndEntity(http.StatusCreated, result) } +// Handles get Deployment detail API call. +func (apiHandler *ApiHandler) handleGetDeploymentDetail( + request *restful.Request, response *restful.Response) { + + namespace := request.PathParameter("namespace") + name := request.PathParameter("deployment") + result, err := deployment.GetDeploymentDetail(apiHandler.client, namespace, + name) + if err != nil { + handleInternalError(response, err) + return + } + + response.WriteHeaderAndEntity(http.StatusOK, result) +} + // Handles get Pod list API call. func (apiHandler *ApiHandler) handleGetPods( request *restful.Request, response *restful.Response) { diff --git a/src/app/backend/resource/deployment/deploymentdetail.go b/src/app/backend/resource/deployment/deploymentdetail.go new file mode 100644 index 000000000000..997f96634ac8 --- /dev/null +++ b/src/app/backend/resource/deployment/deploymentdetail.go @@ -0,0 +1,148 @@ +package deployment + +import ( + "log" + + "github.com/kubernetes/dashboard/resource/common" + "github.com/kubernetes/dashboard/resource/replicaset" + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/apis/extensions" + client "k8s.io/kubernetes/pkg/client/unversioned" + deploymentutil "k8s.io/kubernetes/pkg/util/deployment" +) + +type RollingUpdateStrategy struct { + MaxSurge int `json:"maxSurge"` + MaxUnavailable int `json:"maxUnavailable"` +} + +type StatusInfo struct { + // Total number of desired replicas on the deployment + Replicas int `json:"replicas"` + + // Number of non-terminated pods that have the desired template spec + Updated int `json:"updated"` + + // Number of available pods (ready for at least minReadySeconds) + // targeted by this deployment + Available int `json:"available"` + + // Total number of unavailable pods targeted by this deployment. + Unavailable int `json:"unavailable"` +} + +// ReplicaSetDetail is a presentation layer view of Kubernetes Replica Set resource. This means +type DeploymentDetail struct { + ObjectMeta common.ObjectMeta `json:"objectMeta"` + TypeMeta common.TypeMeta `json:"typeMeta"` + + // Label selector of the service. + Selector map[string]string `json:"selector"` + + // Status information on the deployment + StatusInfo `json:"statusInfo"` + + // The deployment strategy to use to replace existing pods with new ones. + // Valid options: Recreate, RollingUpdate + Strategy string `json:"strategy"` + + // Min ready seconds + MinReadySeconds int `json:"minReadySeconds"` + + // Rolling update strategy containing maxSurge and maxUnavailable + RollingUpdateStrategy `json:"rollingUpdateStrategy,omitempty"` + + // RepliaSetList containing old replica sets from the deployment + OldReplicaSetList replicaset.ReplicaSetList `json:"oldReplicaSetList"` + + // New replica set used by this deployment + NewReplicaSet replicaset.ReplicaSet `json:"newReplicaSet"` + + // List of events related to this Deployment + EventList common.EventList `json:"eventList"` +} + +func GetDeploymentDetail(client client.Interface, namespace string, + name string) (*DeploymentDetail, error) { + + log.Printf("Getting details of %s deployment in %s namespace", name, namespace) + + deploymentData, err := client.Extensions().Deployments(namespace).Get(name) + if err != nil { + return nil, err + } + + channels := &common.ResourceChannels{ + ReplicaSetList: common.GetReplicaSetListChannel(client.Extensions(), 1), + PodList: common.GetPodListChannel(client, 1), + } + + replicaSetList := <-channels.ReplicaSetList.List + if err := <-channels.ReplicaSetList.Error; err != nil { + return nil, err + } + + pods := <-channels.PodList.List + if err := <-channels.PodList.Error; err != nil { + return nil, err + } + + oldReplicaSets, _, err := deploymentutil.FindOldReplicaSets( + deploymentData, replicaSetList.Items, pods) + if err != nil { + return nil, err + } + + newReplicaSet, err := deploymentutil.FindNewReplicaSet(deploymentData, replicaSetList.Items) + if err != nil { + return nil, err + } + + events, err := GetDeploymentEvents(client, namespace, name) + if err != nil { + return nil, err + } + + return getDeploymentDetail(deploymentData, oldReplicaSets, newReplicaSet, + pods.Items, events), nil +} + +func getDeploymentDetail(deployment *extensions.Deployment, + oldRs []*extensions.ReplicaSet, newRs *extensions.ReplicaSet, + pods []api.Pod, events *common.EventList) *DeploymentDetail { + + newRsPodInfo := common.GetPodInfo(newRs.Status.Replicas, newRs.Spec.Replicas, pods) + newReplicaSet := replicaset.ToReplicaSet(newRs, &newRsPodInfo) + + oldReplicaSets := make([]extensions.ReplicaSet, len(oldRs)) + for i, replicaSet := range oldRs { + oldReplicaSets[i] = *replicaSet + } + oldReplicaSetList := replicaset.ToReplicaSetList(oldReplicaSets, + []api.Service{}, pods, []api.Event{}, []api.Node{}) + + return &DeploymentDetail{ + ObjectMeta: common.NewObjectMeta(deployment.ObjectMeta), + TypeMeta: common.NewTypeMeta(common.ResourceKindDeployment), + Selector: deployment.Spec.Selector.MatchLabels, + StatusInfo: GetStatusInfo(&deployment.Status), + Strategy: string(deployment.Spec.Strategy.Type), + MinReadySeconds: deployment.Spec.MinReadySeconds, + RollingUpdateStrategy: RollingUpdateStrategy{ + MaxSurge: deployment.Spec.Strategy.RollingUpdate.MaxSurge.IntValue(), + MaxUnavailable: deployment.Spec.Strategy.RollingUpdate.MaxUnavailable.IntValue(), + }, + OldReplicaSetList: *oldReplicaSetList, + NewReplicaSet: newReplicaSet, + EventList: *events, + } +} + +func GetStatusInfo(deploymentStatus *extensions.DeploymentStatus) StatusInfo { + return StatusInfo{ + Replicas: deploymentStatus.Replicas, + Updated: deploymentStatus.UpdatedReplicas, + Available: deploymentStatus.AvailableReplicas, + Unavailable: deploymentStatus.UnavailableReplicas, + } +} diff --git a/src/app/backend/resource/deployment/deploymentevents.go b/src/app/backend/resource/deployment/deploymentevents.go new file mode 100644 index 000000000000..01c6047c8fee --- /dev/null +++ b/src/app/backend/resource/deployment/deploymentevents.go @@ -0,0 +1,35 @@ +package deployment + +import ( + "log" + + "github.com/kubernetes/dashboard/resource/common" + "github.com/kubernetes/dashboard/resource/event" + client "k8s.io/kubernetes/pkg/client/unversioned" +) + +func GetDeploymentEvents(client client.Interface, namespace string, deploymentName string) (*common.EventList, error) { + + log.Printf("Getting events related to %s deployment in %s namespace", deploymentName, + namespace) + + // Get events for deployment. + dpEvents, err := event.GetEvents(client, namespace, deploymentName) + if err != nil { + return nil, err + } + + if !event.IsTypeFilled(dpEvents) { + dpEvents = event.FillEventsType(dpEvents) + } + + events := event.AppendEvents(dpEvents, common.EventList{ + Namespace: namespace, + Events: make([]common.Event, 0), + }) + + log.Printf("Found %d events related to %s deployment in %s namespace", + len(events.Events), deploymentName, namespace) + + return &events, nil +} diff --git a/src/app/backend/resource/replicaset/replicasetlist.go b/src/app/backend/resource/replicaset/replicasetlist.go index fc6922f20af4..f55476939ade 100644 --- a/src/app/backend/resource/replicaset/replicasetlist.go +++ b/src/app/backend/resource/replicaset/replicasetlist.go @@ -99,11 +99,11 @@ func GetReplicaSetListFromChannels(channels *common.ResourceChannels) ( return nil, err } - return getReplicaSetList(replicaSets.Items, services.Items, pods.Items, events.Items, + return ToReplicaSetList(replicaSets.Items, services.Items, pods.Items, events.Items, nodes.Items), nil } -func getReplicaSetList(replicaSets []extensions.ReplicaSet, +func ToReplicaSetList(replicaSets []extensions.ReplicaSet, services []api.Service, pods []api.Pod, events []api.Event, nodes []api.Node) *ReplicaSetList { @@ -116,14 +116,17 @@ func getReplicaSetList(replicaSets []extensions.ReplicaSet, replicaSet.Spec.Selector.MatchLabels) podInfo := getPodInfo(&replicaSet, matchingPods) - replicaSetList.ReplicaSets = append(replicaSetList.ReplicaSets, - ReplicaSet{ - ObjectMeta: common.NewObjectMeta(replicaSet.ObjectMeta), - TypeMeta: common.NewTypeMeta(common.ResourceKindReplicaSet), - ContainerImages: common.GetContainerImages(&replicaSet.Spec.Template.Spec), - Pods: podInfo, - }) + replicaSetList.ReplicaSets = append(replicaSetList.ReplicaSets, ToReplicaSet(&replicaSet, &podInfo)) } return replicaSetList } + +func ToReplicaSet(replicaSet *extensions.ReplicaSet, podInfo *common.PodInfo) ReplicaSet { + return ReplicaSet{ + ObjectMeta: common.NewObjectMeta(replicaSet.ObjectMeta), + TypeMeta: common.NewTypeMeta(common.ResourceKindReplicaSet), + ContainerImages: common.GetContainerImages(&replicaSet.Spec.Template.Spec), + Pods: *podInfo, + } +} diff --git a/src/test/backend/resource/deployment/deploymentdetail_test.go b/src/test/backend/resource/deployment/deploymentdetail_test.go new file mode 100644 index 000000000000..780950228c7b --- /dev/null +++ b/src/test/backend/resource/deployment/deploymentdetail_test.go @@ -0,0 +1,139 @@ +package deployment + +import ( + "reflect" + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/client/unversioned/testclient" + deploymentutil "k8s.io/kubernetes/pkg/util/deployment" + "k8s.io/kubernetes/pkg/util/intstr" + + "github.com/kubernetes/dashboard/resource/common" + "github.com/kubernetes/dashboard/resource/replicaset" +) + +func TestGetDeploymentDetail(t *testing.T) { + podList := &api.PodList{} + eventList := &api.EventList{} + + deployment := &extensions.Deployment{ + ObjectMeta: api.ObjectMeta{ + Name: "test-name", + Labels: map[string]string{"track": "beta"}, + }, + Spec: extensions.DeploymentSpec{ + Selector: &unversioned.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}, + Replicas: 4, + MinReadySeconds: 5, + Strategy: extensions.DeploymentStrategy{ + Type: extensions.RollingUpdateDeploymentStrategyType, + RollingUpdate: &extensions.RollingUpdateDeployment{ + MaxSurge: intstr.FromInt(1), + MaxUnavailable: intstr.FromString("1"), + }, + }, + Template: api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Name: "test-pod-name", + Labels: map[string]string{"track": "beta"}, + }, + }, + }, + Status: extensions.DeploymentStatus{ + Replicas: 4, + UpdatedReplicas: 2, + AvailableReplicas: 3, + UnavailableReplicas: 1, + }, + } + + podTemplateSpec := deploymentutil.GetNewReplicaSetTemplate(deployment) + + newReplicaSet := extensions.ReplicaSet{ + ObjectMeta: api.ObjectMeta{Name: "replica-set-1"}, + Spec: extensions.ReplicaSetSpec{ + Template: podTemplateSpec, + }, + } + + replicaSetList := &extensions.ReplicaSetList{ + Items: []extensions.ReplicaSet{ + newReplicaSet, + { + ObjectMeta: api.ObjectMeta{Name: "replica-set-2"}, + }, + }, + } + + cases := []struct { + namespace, name string + expectedActions []string + deployment *extensions.Deployment + expected *DeploymentDetail + }{ + { + "test-namespace", "test-name", + []string{"get", "list", "list", "list"}, + deployment, + &DeploymentDetail{ + ObjectMeta: common.ObjectMeta{ + Name: "test-name", + Labels: map[string]string{"track": "beta"}, + }, + TypeMeta: common.TypeMeta{Kind: common.ResourceKindDeployment}, + Selector: map[string]string{"foo": "bar"}, + StatusInfo: StatusInfo{ + Replicas: 4, + Updated: 2, + Available: 3, + Unavailable: 1, + }, + Strategy: "RollingUpdate", + MinReadySeconds: 5, + RollingUpdateStrategy: RollingUpdateStrategy{ + MaxSurge: 1, + MaxUnavailable: 1, + }, + OldReplicaSetList: replicaset.ReplicaSetList{ReplicaSets: []replicaset.ReplicaSet{}}, + NewReplicaSet: replicaset.ReplicaSet{ + ObjectMeta: common.NewObjectMeta(newReplicaSet.ObjectMeta), + TypeMeta: common.NewTypeMeta(common.ResourceKindReplicaSet), + Pods: common.PodInfo{Warnings: []common.Event{}}, + }, + EventList: common.EventList{ + Namespace: "test-namespace", + Events: []common.Event{}, + }, + }, + }, + } + + for _, c := range cases { + + fakeClient := testclient.NewSimpleFake(c.deployment, replicaSetList, podList, eventList) + + actual, _ := GetDeploymentDetail(fakeClient, c.namespace, c.name) + + 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("GetDeploymentDetail(client, namespace, name) == \ngot: %#v, \nexpected %#v", + actual, c.expected) + } + } +} diff --git a/src/test/backend/resource/deployment/deploymentevents_test.go b/src/test/backend/resource/deployment/deploymentevents_test.go new file mode 100644 index 000000000000..8d1e06639bae --- /dev/null +++ b/src/test/backend/resource/deployment/deploymentevents_test.go @@ -0,0 +1,67 @@ +package deployment + +import ( + "reflect" + "testing" + + "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/unversioned" + "k8s.io/kubernetes/pkg/apis/extensions" + "k8s.io/kubernetes/pkg/client/unversioned/testclient" + + "github.com/kubernetes/dashboard/resource/common" +) + +func TestGetDeploymentEvents(t *testing.T) { + cases := []struct { + namespace, name string + eventList *api.EventList + deployment *extensions.Deployment + expectedActions []string + expected *common.EventList + }{ + { + "test-namespace", "test-name", + &api.EventList{Items: []api.Event{{Message: "test-message"}}}, + &extensions.Deployment{ + ObjectMeta: api.ObjectMeta{Name: "test-replicaset"}, + Spec: extensions.DeploymentSpec{ + Selector: &unversioned.LabelSelector{ + MatchLabels: map[string]string{}, + }}}, + []string{"list"}, + &common.EventList{ + Namespace: "test-namespace", + Events: []common.Event{{ + TypeMeta: common.TypeMeta{common.ResourceKindEvent}, + Message: "test-message", + Type: api.EventTypeNormal, + }}}, + }, + } + + for _, c := range cases { + fakeClient := testclient.NewSimpleFake(c.eventList, c.deployment) + + actual, _ := GetDeploymentEvents(fakeClient, c.namespace, c.name) + + 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("GetDeploymentEvents(client,%#v, %#v) == \ngot: %#v, \nexpected %#v", + c.namespace, c.name, actual, c.expected) + } + } +}