diff --git a/references/cli/top/config/color.go b/references/cli/top/config/color.go index a144bd105..c82514f3f 100644 --- a/references/cli/top/config/color.go +++ b/references/cli/top/config/color.go @@ -51,4 +51,12 @@ const ( ObjectProgressingStatusColor = "[blue::]" // ObjectUnKnownStatusColor is object UnKnown status text color ObjectUnKnownStatusColor = "[gray::]" + // PodPendingPhaseColor is pod pending phase text color + PodPendingPhaseColor = "[yellow::]" + // PodRunningPhaseColor is pod running phase text color + PodRunningPhaseColor = "[green::]" + // PodSucceededPhase is pod succeeded phase text color + PodSucceededPhase = "[purple::]" + // PodFailedPhase is pod failed phase text color + PodFailedPhase = "[red::]" ) diff --git a/references/cli/top/model/cluster_namespace.go b/references/cli/top/model/cluster_namespace.go index d6497eb72..c7b651606 100644 --- a/references/cli/top/model/cluster_namespace.go +++ b/references/cli/top/model/cluster_namespace.go @@ -23,6 +23,8 @@ import ( v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/references/cli/top/utils" ) // NamespaceList is namespace list @@ -58,7 +60,7 @@ func ListClusterNamespaces(ctx context.Context, c client.Client) (*NamespaceList list.data = append(list.data, Namespace{ Name: namespaceInfo.Name, Status: string(namespaceInfo.Status.Phase), - Age: timeFormat(time.Since(namespaceInfo.CreationTimestamp.Time)), + Age: utils.TimeFormat(time.Since(namespaceInfo.CreationTimestamp.Time)), }) } diff --git a/references/cli/top/model/managed_resource_test.go b/references/cli/top/model/managed_resource_test.go index 8642c3be2..d38868caa 100644 --- a/references/cli/top/model/managed_resource_test.go +++ b/references/cli/top/model/managed_resource_test.go @@ -25,28 +25,28 @@ import ( "github.com/stretchr/testify/assert" ) -func TestK8SObjectList_Header(t *testing.T) { +func TestManagedResource_Header(t *testing.T) { list := ManagedResourceList{title: []string{"name", "namespace", "kind", "APIVersion", "cluster", "status"}} assert.Equal(t, len(list.Header()), 6) assert.Equal(t, list.Header(), []string{"name", "namespace", "kind", "APIVersion", "cluster", "status"}) } -func TestK8SObjectList_Body(t *testing.T) { +func TestManagedResource_Body(t *testing.T) { list := ManagedResourceList{data: []ManagedResource{{"", "", "", "", "", ""}}} assert.Equal(t, len(list.Body()), 1) assert.Equal(t, list.Body(), [][]string{{"", "", "", "", "", ""}}) } -var _ = Describe("test k8s object", func() { +var _ = Describe("test managed resource", func() { ctx := context.Background() ctx = context.WithValue(ctx, &CtxKeyAppName, "first-vela-app") ctx = context.WithValue(ctx, &CtxKeyNamespace, "default") ctx = context.WithValue(ctx, &CtxKeyCluster, "local") - It("list k8s object", func() { + It("list managed resource", func() { list, err := ListManagedResource(ctx, k8sClient) Expect(err).NotTo(HaveOccurred()) Expect(len(list.Header())).To(Equal(6)) - Expect(len(list.Body())).To(Equal(2)) + Expect(len(list.Body())).To(Equal(4)) }) }) diff --git a/references/cli/top/model/namespace.go b/references/cli/top/model/namespace.go index 809e83b0a..68a85ff82 100644 --- a/references/cli/top/model/namespace.go +++ b/references/cli/top/model/namespace.go @@ -18,13 +18,12 @@ package model import ( "context" - "fmt" - "strconv" - "strings" "time" v1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/references/cli/top/utils" ) // Namespace is namespace struct @@ -48,7 +47,7 @@ func ListNamespaces(ctx context.Context, c client.Reader) *NamespaceList { list.data = append(list.data, Namespace{ Name: ns.Name, Status: string(ns.Status.Phase), - Age: timeFormat(time.Since(ns.CreationTimestamp.Time)), + Age: utils.TimeFormat(time.Since(ns.CreationTimestamp.Time)), }) } return list @@ -67,20 +66,3 @@ func (l *NamespaceList) Body() [][]string { } return data } - -// timeFormat format time data of `time.Duration` type to string type -func timeFormat(t time.Duration) string { - str := t.String() - // remove "." - tmp := strings.Split(str, ".") - tmp[0] += "s" - - tmp = strings.Split(tmp[0], "h") - // hour num - hour, err := strconv.Atoi(tmp[0]) - if err != nil { - return "" - } - - return fmt.Sprintf("%dd%dh%2s", hour/24, hour%24, tmp[1]) -} diff --git a/references/cli/top/model/namespace_test.go b/references/cli/top/model/namespace_test.go index 309a2fa86..d1f629536 100644 --- a/references/cli/top/model/namespace_test.go +++ b/references/cli/top/model/namespace_test.go @@ -19,7 +19,6 @@ package model import ( "context" "testing" - "time" . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" @@ -43,15 +42,6 @@ func TestNamespaceList_Body(t *testing.T) { assert.Equal(t, nsList.Body()[0], []string{AllNamespace, "*", "*"}) } -func TestTimeFormat(t *testing.T) { - t1, err1 := time.ParseDuration("1.5h") - assert.NoError(t, err1) - assert.Equal(t, timeFormat(t1), "0d1h30m0ss") - t2, err2 := time.ParseDuration("25h") - assert.NoError(t, err2) - assert.Equal(t, timeFormat(t2), "1d1h0m0ss") -} - var _ = Describe("test namespace", func() { ctx := context.Background() It("list namespace", func() { diff --git a/references/cli/top/model/pod.go b/references/cli/top/model/pod.go new file mode 100644 index 000000000..e3c107524 --- /dev/null +++ b/references/cli/top/model/pod.go @@ -0,0 +1,142 @@ +/* +Copyright 2022 The KubeVela Authors. + +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 model + +import ( + "context" + "fmt" + "strconv" + "time" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/pkg/velaql/providers/query" + "github.com/oam-dev/kubevela/references/cli/top/utils" +) + +// Pod represent the k8s pod resource instance +type Pod struct { + Name string + Namespace string + Ready string + Status string + CPU string + Mem string + CPUR string + CPUL string + MemR string + MemL string + IP string + NodeName string + Age string +} + +// PodList is pod list +type PodList struct { + title []string + data []Pod +} + +// ListPods return pod list of component +func ListPods(ctx context.Context, cfg *rest.Config, c client.Client) (*PodList, error) { + list := &PodList{title: []string{"Name", "Namespace", "Ready", "Status", "CPU", "MEM", "%CPU/R", "%CPU/L", "%MEM/R", "%MEM/L", "IP", "Node", "Age"}, data: []Pod{}} + appName := ctx.Value(&CtxKeyAppName).(string) + appNamespace := ctx.Value(&CtxKeyNamespace).(string) + compCluster := ctx.Value(&CtxKeyCluster).(string) + compNamespace := ctx.Value(&CtxKeyClusterNamespace).(string) + compName := ctx.Value(&CtxKeyComponentName).(string) + + opt := query.Option{ + Name: appName, + Namespace: appNamespace, + Filter: query.FilterOption{ + Cluster: compCluster, + ClusterNamespace: compNamespace, + Components: []string{compName}, + APIVersion: "v1", + Kind: "Pod", + }, + WithTree: true, + } + resource, err := collectResource(ctx, c, opt) + if err != nil { + return list, err + } + for _, object := range resource { + pod := &v1.Pod{} + err = runtime.DefaultUnstructuredConverter.FromUnstructured(object.UnstructuredContent(), pod) + if err != nil { + continue + } + list.data = append(list.data, LoadPodDetail(cfg, pod)) + } + return list, nil +} + +// LoadPodDetail gather the pod detail info +func LoadPodDetail(cfg *rest.Config, pod *v1.Pod) Pod { + podInfo := Pod{ + Name: pod.Name, + Namespace: pod.Namespace, + Ready: readyContainerNum(pod), + Status: string(pod.Status.Phase), + Age: utils.TimeFormat(time.Since(pod.CreationTimestamp.Time)), + IP: pod.Status.PodIP, + NodeName: pod.Spec.NodeName, + } + metric, err := utils.PodMetric(cfg, pod.Name, pod.Namespace) + if err != nil { + podInfo.CPU, podInfo.Mem, podInfo.CPUL, podInfo.MemL, podInfo.CPUR, podInfo.MemR = utils.NA, utils.NA, utils.NA, utils.NA, utils.NA, utils.NA + } else { + c, r := utils.GatherPodMX(pod, metric) + podInfo.CPU, podInfo.Mem = strconv.FormatInt(c.CPU, 10), strconv.FormatInt(c.Mem/1000000, 10) + podInfo.CPUR = utils.ToPercentageStr(c.CPU, r.CPU) + podInfo.MemR = utils.ToPercentageStr(c.Mem, r.Mem) + podInfo.CPUL = utils.ToPercentageStr(c.CPU, r.Lcpu) + podInfo.MemL = utils.ToPercentageStr(c.CPU, r.Lmem) + } + + return podInfo +} + +func readyContainerNum(pod *v1.Pod) string { + total := len(pod.Status.ContainerStatuses) + ready := 0 + for _, c := range pod.Status.ContainerStatuses { + if c.Ready { + ready++ + } + } + return fmt.Sprintf("%d/%d", ready, total) +} + +// Header generate header of table in pod view +func (l *PodList) Header() []string { + return l.title +} + +// Body generate body of table in pod view +func (l *PodList) Body() [][]string { + data := make([][]string, 0) + for _, pod := range l.data { + data = append(data, []string{pod.Name, pod.Namespace, pod.Ready, pod.Status, pod.CPU, pod.Mem, pod.CPUR, pod.MemR, pod.CPUL, pod.MemL, pod.IP, pod.NodeName, pod.Age}) + } + return data +} diff --git a/references/cli/top/model/pod_test.go b/references/cli/top/model/pod_test.go new file mode 100644 index 000000000..7cf4e4fb2 --- /dev/null +++ b/references/cli/top/model/pod_test.go @@ -0,0 +1,87 @@ +/* +Copyright 2022 The KubeVela Authors. + +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 model + +import ( + "context" + "testing" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestPod(t *testing.T) { + pod := Pod{ + Name: "", + Namespace: "", + Ready: "", + Status: "", + CPU: "", + Mem: "", + CPUR: "", + CPUL: "", + MemR: "", + MemL: "", + IP: "", + NodeName: "", + Age: "", + } + podList := &PodList{title: []string{"Name", "Namespace", "Ready", "Status", "CPU", "MEM", "%CPU/R", "%CPU/L", "%MEM/R", "%MEM/L", "IP", "Node", "Age"}, data: []Pod{pod}} + assert.Equal(t, len(podList.Header()), 13) + assert.Equal(t, podList.Header()[0], "Name") + assert.Equal(t, len(podList.Body()), 1) +} + +var _ = Describe("test pod", func() { + ctx := context.Background() + ctx = context.WithValue(ctx, &CtxKeyAppName, "first-vela-app") + ctx = context.WithValue(ctx, &CtxKeyNamespace, "default") + ctx = context.WithValue(ctx, &CtxKeyCluster, "") + ctx = context.WithValue(ctx, &CtxKeyClusterNamespace, "") + ctx = context.WithValue(ctx, &CtxKeyComponentName, "deploy1") + + It("list pods", func() { + podList, err := ListPods(ctx, cfg, k8sClient) + Expect(err).NotTo(HaveOccurred()) + Expect(len(podList.Body())).To(Equal(1)) + }) + + It("load pod detail", func() { + pod := &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod", + Namespace: "ns", + CreationTimestamp: metav1.Time{Time: time.Now()}, + }, + Spec: v1.PodSpec{ + NodeName: "node-1", + }, + Status: v1.PodStatus{ + Phase: "running", + PodIP: "10.1.1.1", + ContainerStatuses: []v1.ContainerStatus{{Ready: true}}, + }, + } + podInfo := LoadPodDetail(cfg, pod) + Expect(podInfo.Ready).To(Equal("1/1")) + Expect(podInfo.IP).To(Equal("10.1.1.1")) + }) +}) diff --git a/references/cli/top/model/resource.go b/references/cli/top/model/resource.go index f487a21c5..fb5100ac4 100644 --- a/references/cli/top/model/resource.go +++ b/references/cli/top/model/resource.go @@ -16,6 +16,18 @@ limitations under the License. package model +import ( + "context" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + apimachinerytypes "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/velaql/providers/query" + querytypes "github.com/oam-dev/kubevela/pkg/velaql/providers/query/types" +) + // ResourceList an abstract kinds of resource list which can convert it to data of view in the form of table type ResourceList interface { // Header generate header of table in resource view @@ -23,3 +35,44 @@ type ResourceList interface { // Body generate body of table in resource view Body() [][]string } + +func collectResource(ctx context.Context, c client.Client, opt query.Option) ([]unstructured.Unstructured, error) { + app := new(v1beta1.Application) + appKey := client.ObjectKey{Name: opt.Name, Namespace: opt.Namespace} + if err := c.Get(context.Background(), appKey, app); err != nil { + return nil, err + } + collector := query.NewAppCollector(c, opt) + appResList, err := collector.ListApplicationResources(context.Background(), app, opt.WithTree) + if err != nil { + return nil, err + } + var resources = make([]unstructured.Unstructured, 0) + for _, res := range appResList { + if res.ResourceTree != nil { + resources = append(resources, sonLeafResource(*res, res.ResourceTree, opt.Filter.Kind, opt.Filter.APIVersion)...) + } + if (opt.Filter.Kind == "" && opt.Filter.APIVersion == "") || (res.Kind == opt.Filter.Kind && res.APIVersion == opt.Filter.APIVersion) { + var object unstructured.Unstructured + object.SetAPIVersion(opt.Filter.APIVersion) + object.SetKind(opt.Filter.Kind) + if err := c.Get(ctx, apimachinerytypes.NamespacedName{Namespace: res.Namespace, Name: res.Name}, &object); err == nil { + resources = append(resources, object) + } + } + } + return resources, nil +} + +func sonLeafResource(res querytypes.AppliedResource, node *querytypes.ResourceTreeNode, kind string, apiVersion string) []unstructured.Unstructured { + objects := make([]unstructured.Unstructured, 0) + if node.LeafNodes != nil { + for i := 0; i < len(node.LeafNodes); i++ { + objects = append(objects, sonLeafResource(res, node.LeafNodes[i], kind, apiVersion)...) + } + } + if (kind == "" && apiVersion == "") || (node.Kind == kind && node.APIVersion == apiVersion) { + objects = append(objects, node.Object) + } + return objects +} diff --git a/references/cli/top/model/resource_test.go b/references/cli/top/model/resource_test.go new file mode 100644 index 000000000..5845b58a5 --- /dev/null +++ b/references/cli/top/model/resource_test.go @@ -0,0 +1,73 @@ +/* +Copyright 2022 The KubeVela Authors. + +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 model + +import ( + "context" + + "testing" + + "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/oam-dev/kubevela/pkg/velaql/providers/query" + querytypes "github.com/oam-dev/kubevela/pkg/velaql/providers/query/types" +) + +var _ = Describe("test resource", func() { + ctx := context.Background() + ctx = context.WithValue(ctx, &CtxKeyAppName, "first-vela-app") + ctx = context.WithValue(ctx, &CtxKeyNamespace, "default") + ctx = context.WithValue(ctx, &CtxKeyCluster, "") + ctx = context.WithValue(ctx, &CtxKeyClusterNamespace, "") + ctx = context.WithValue(ctx, &CtxKeyComponentName, "webservice-test") + + opt := query.Option{ + Name: "first-vela-app", + Namespace: "default", + Filter: query.FilterOption{ + Cluster: "", + ClusterNamespace: "", + Components: []string{"deploy1"}, + APIVersion: "v1", + Kind: "Pod", + }, + WithTree: true, + } + + It("collect resource", func() { + podList, err := collectResource(ctx, k8sClient, opt) + Expect(err).NotTo(HaveOccurred()) + Expect(len(podList)).To(Equal(1)) + }) +}) + +func TestSonLeafResource(t *testing.T) { + res := querytypes.AppliedResource{} + node := &querytypes.ResourceTreeNode{ + LeafNodes: []*querytypes.ResourceTreeNode{ + { + Object: unstructured.Unstructured{}, + }, + }, + } + objs := sonLeafResource(res, node, "", "") + assert.Equal(t, len(objs), 2) +} diff --git a/references/cli/top/model/suit_test.go b/references/cli/top/model/suit_test.go index b7a8d4618..78977c88e 100644 --- a/references/cli/top/model/suit_test.go +++ b/references/cli/top/model/suit_test.go @@ -25,14 +25,11 @@ import ( "k8s.io/apimachinery/pkg/api/resource" - "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" - "github.com/oam-dev/kubevela/apis/types" - . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/rest" "k8s.io/utils/pointer" @@ -40,6 +37,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/envtest" common2 "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/apis/types" helmapi "github.com/oam-dev/kubevela/pkg/appfile/helm/flux2apis" "github.com/oam-dev/kubevela/pkg/oam" "github.com/oam-dev/kubevela/pkg/utils/common" @@ -49,23 +48,8 @@ var cfg *rest.Config var k8sClient client.Client var testEnv *envtest.Environment -var createObject = func(name string, ns string, value string, kind string) *unstructured.Unstructured { - o := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "metadata": map[string]interface{}{ - "name": name, - "namespace": ns, - }, - "data": map[string]interface{}{ - "key": value, - }, - }, - } - o.SetGroupVersionKind(corev1.SchemeGroupVersion.WithKind(kind)) - return o -} - var _ = BeforeSuite(func(done Done) { + // env init By("bootstrapping test environment") testEnv = &envtest.Environment{ ControlPlaneStartTimeout: time.Minute * 3, @@ -75,19 +59,19 @@ var _ = BeforeSuite(func(done Done) { "../../../../charts/vela-core/crds", }, } - + // env start By("start kube test env") var err error cfg, err = testEnv.Start() Expect(err).ShouldNot(HaveOccurred()) Expect(cfg).ToNot(BeNil()) - + // client start By("new kube client") cfg.Timeout = time.Minute * 2 k8sClient, err = client.New(cfg, client.Options{Scheme: common.Scheme}) Expect(err).Should(BeNil()) Expect(k8sClient).ToNot(BeNil()) - + // create app name, namespace := "first-vela-app", "default" testApp := &v1beta1.Application{ ObjectMeta: metav1.ObjectMeta{ @@ -165,14 +149,97 @@ var _ = BeforeSuite(func(done Done) { } err = k8sClient.Status().Update(context.TODO(), testApp) Expect(err).Should(BeNil()) - - svc := createObject("service1", namespace, "x", "Service") + // create service + svc := &corev1.Service{ + TypeMeta: metav1.TypeMeta{APIVersion: "v1", Kind: "Service"}, + ObjectMeta: metav1.ObjectMeta{ + Name: "service1", + Namespace: namespace, + }, + Spec: corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 2002}}, + }, + } svcRaw, err := json.Marshal(svc) Expect(err).Should(Succeed()) - dply := createObject("deploy1", namespace, "y", "Deployment") + Expect(k8sClient.Create(context.TODO(), svc)).Should(BeNil()) + // create deploy + dply := &appsv1.Deployment{ + TypeMeta: metav1.TypeMeta{ + Kind: "Deployment", + APIVersion: "apps/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "deploy1", + Namespace: namespace, + }, + Spec: appsv1.DeploymentSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "test"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: namespace, Labels: map[string]string{"app": "test"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{ + { + Name: "vela-core-1", + Image: "vela", + }, + }}, + }, + }, + } dplyRaw, err := json.Marshal(dply) Expect(err).Should(Succeed()) - + Expect(k8sClient.Create(context.TODO(), dply)).Should(BeNil()) + //create replicaSet + var rsNum int32 = 2 + rs := &appsv1.ReplicaSet{ + TypeMeta: metav1.TypeMeta{ + Kind: "ReplicaSet", + APIVersion: "apps/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "rs1", + Namespace: namespace, + Labels: map[string]string{"app": "test"}, + }, + Spec: appsv1.ReplicaSetSpec{ + Replicas: &rsNum, + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": "test"}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: namespace, Labels: map[string]string{"app": "test"}}, + Spec: corev1.PodSpec{Containers: []corev1.Container{ + { + Name: "vela-core-1", + Image: "vela", + }, + }}, + }, + }, + } + rsRaw, err := json.Marshal(rs) + Expect(err).Should(Succeed()) + Expect(k8sClient.Create(context.TODO(), rs)).Should(BeNil()) + // create pod + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod", + Namespace: namespace, + Labels: map[string]string{"app": "test"}, + }, + Spec: corev1.PodSpec{Containers: []corev1.Container{ + { + Name: "vela-core-1", + Image: "vela", + }, + }}, + } + podRaw, err := json.Marshal(pod) + Expect(err).Should(Succeed()) + Expect(k8sClient.Create(context.TODO(), pod)).Should(BeNil()) + // create resourceTracker rt := &v1beta1.ResourceTracker{ ObjectMeta: metav1.ObjectMeta{ Name: fmt.Sprintf("%s-v1-%s", name, namespace), @@ -193,11 +260,11 @@ var _ = BeforeSuite(func(done Done) { APIVersion: "v1", Kind: "Service", Namespace: namespace, - Name: "web", + Name: "service1", }, }, OAMObjectReference: common2.OAMObjectReference{ - Component: "web", + Component: "service1", }, Data: &runtime.RawExtension{Raw: svcRaw}, }, @@ -208,14 +275,44 @@ var _ = BeforeSuite(func(done Done) { APIVersion: "apps/v1", Kind: "Deployment", Namespace: namespace, - Name: "web", + Name: "deploy1", }, }, OAMObjectReference: common2.OAMObjectReference{ - Component: "web", + Component: "deploy1", }, Data: &runtime.RawExtension{Raw: dplyRaw}, }, + { + ClusterObjectReference: common2.ClusterObjectReference{ + Cluster: "", + ObjectReference: corev1.ObjectReference{ + APIVersion: "apps/v1", + Kind: "ReplicaSet", + Namespace: namespace, + Name: "rs1", + }, + }, + OAMObjectReference: common2.OAMObjectReference{ + Component: "rs1", + }, + Data: &runtime.RawExtension{Raw: rsRaw}, + }, + { + ClusterObjectReference: common2.ClusterObjectReference{ + Cluster: "", + ObjectReference: corev1.ObjectReference{ + APIVersion: "apps/v1", + Kind: "Pod", + Namespace: namespace, + Name: "pod1", + }, + }, + OAMObjectReference: common2.OAMObjectReference{ + Component: "pod1", + }, + Data: &runtime.RawExtension{Raw: podRaw}, + }, }, Type: v1beta1.ResourceTrackerTypeVersioned, }, @@ -233,8 +330,6 @@ var _ = BeforeSuite(func(done Done) { quantityLimitsMemory, _ := resource.ParseQuantity("10Mi") quantityRequestsCPU, _ := resource.ParseQuantity("10m") quantityRequestsMemory, _ := resource.ParseQuantity("10Mi") - //quantityUsageCPU, _ := resource.ParseQuantity("8m") - //quantityUsageMemory, _ := resource.ParseQuantity("20Mi") pod1 := &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{Name: "vela-core", Namespace: "vela-system", Labels: map[string]string{"app.kubernetes.io/name": "vela-core"}}, diff --git a/references/cli/top/model/type.go b/references/cli/top/model/type.go index d1ad2a2e5..a50b7da9e 100644 --- a/references/cli/top/model/type.go +++ b/references/cli/top/model/type.go @@ -58,6 +58,8 @@ var ( CtxKeyCluster = "cluster" // CtxKeyClusterNamespace request context key of cluster namespace name CtxKeyClusterNamespace = "cluster" + // CtxKeyComponentName request context key of component name + CtxKeyComponentName = "componentName" ) const ( diff --git a/references/cli/top/utils/metrics.go b/references/cli/top/utils/metrics.go index c22e513b9..96097f51b 100644 --- a/references/cli/top/utils/metrics.go +++ b/references/cli/top/utils/metrics.go @@ -17,12 +17,16 @@ limitations under the License. package utils import ( + "context" "math" "strconv" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" + apiv1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/rest" "k8s.io/metrics/pkg/apis/metrics/v1beta1" + metrics "k8s.io/metrics/pkg/client/clientset/versioned" ) const ( @@ -101,6 +105,20 @@ func podRequests(spec v1.PodSpec) (*resource.Quantity, *resource.Quantity) { return cpu, mem } +// PodMetric return the pod metric +func PodMetric(cfg *rest.Config, name, namespace string) (*v1beta1.PodMetrics, error) { + ctx := context.Background() + c, err := metrics.NewForConfig(cfg) + if err != nil { + return nil, err + } + metric, err := c.MetricsV1beta1().PodMetricses(namespace).Get(ctx, name, apiv1.GetOptions{}) + if err != nil { + return nil, err + } + return metric, nil +} + // ToPercentage computes percentage as string otherwise n/aa. func ToPercentage(v1, v2 int64) int { if v2 == 0 { diff --git a/references/cli/top/utils/time.go b/references/cli/top/utils/time.go new file mode 100644 index 000000000..7ad3d90c6 --- /dev/null +++ b/references/cli/top/utils/time.go @@ -0,0 +1,40 @@ +/* +Copyright 2022 The KubeVela Authors. + +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 utils + +import ( + "fmt" + "strconv" + "strings" + "time" +) + +// TimeFormat format time data of `time.Duration` type to string type +func TimeFormat(t time.Duration) string { + str := t.String() + // remove "." + tmp := strings.Split(str, ".") + tmp[0] += "s" + + tmp = strings.Split(tmp[0], "h") + // hour num + hour, err := strconv.Atoi(tmp[0]) + if err != nil { + return "" + } + return fmt.Sprintf("%dd%dh%2s", hour/24, hour%24, tmp[1]) +} diff --git a/references/cli/top/utils/time_test.go b/references/cli/top/utils/time_test.go new file mode 100644 index 000000000..5df056759 --- /dev/null +++ b/references/cli/top/utils/time_test.go @@ -0,0 +1,33 @@ +/* +Copyright 2022 The KubeVela Authors. + +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 utils + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestTimeFormat(t *testing.T) { + t1, err1 := time.ParseDuration("1.5h") + assert.NoError(t, err1) + assert.Equal(t, TimeFormat(t1), "0d1h30m0ss") + t2, err2 := time.ParseDuration("25h") + assert.NoError(t, err2) + assert.Equal(t, TimeFormat(t2), "1d1h0m0ss") +} diff --git a/references/cli/top/view/managed_resource_view.go b/references/cli/top/view/managed_resource_view.go index d11ab3b0e..7cb9e5601 100644 --- a/references/cli/top/view/managed_resource_view.go +++ b/references/cli/top/view/managed_resource_view.go @@ -111,6 +111,7 @@ func (v *ManagedResourceView) ColorizeStatusText(rowNum int) { func (v *ManagedResourceView) bindKeys() { v.Actions().Delete([]tcell.Key{tcell.KeyEnter}) v.Actions().Add(model.KeyActions{ + tcell.KeyEnter: model.KeyAction{Description: "Enter", Action: v.podView, Visible: true, Shared: true}, component.KeyC: model.KeyAction{Description: "Select Cluster", Action: v.clusterView, Visible: true, Shared: true}, component.KeyN: model.KeyAction{Description: "Select ClusterNS", Action: v.clusterNamespaceView, Visible: true, Shared: true}, tcell.KeyESC: model.KeyAction{Description: "Back", Action: v.app.Back, Visible: true, Shared: true}, @@ -131,3 +132,17 @@ func (v *ManagedResourceView) clusterNamespaceView(event *tcell.EventKey) *tcell v.app.command.run(v.ctx, "cns") return event } + +func (v *ManagedResourceView) podView(event *tcell.EventKey) *tcell.EventKey { + row, _ := v.GetSelection() + if row == 0 { + return event + } + name, namespace, cluster := v.GetCell(row, 0).Text, v.GetCell(row, 1).Text, v.GetCell(row, 4).Text + v.ctx = context.WithValue(v.ctx, &model.CtxKeyCluster, cluster) + v.ctx = context.WithValue(v.ctx, &model.CtxKeyClusterNamespace, namespace) + v.ctx = context.WithValue(v.ctx, &model.CtxKeyComponentName, name) + + v.app.command.run(v.ctx, "pod") + return nil +} diff --git a/references/cli/top/view/managed_resource_view_test.go b/references/cli/top/view/managed_resource_view_test.go index f01b27d16..62d057a92 100644 --- a/references/cli/top/view/managed_resource_view_test.go +++ b/references/cli/top/view/managed_resource_view_test.go @@ -31,7 +31,7 @@ import ( "github.com/oam-dev/kubevela/references/cli/top/model" ) -func TestK8SView(t *testing.T) { +func TestManagedResourceView(t *testing.T) { testEnv := &envtest.Environment{ ControlPlaneStartTimeout: time.Minute * 3, ControlPlaneStopTimeout: time.Minute, @@ -49,13 +49,13 @@ func TestK8SView(t *testing.T) { ctx = context.WithValue(ctx, &model.CtxKeyCluster, "") view := NewManagedResourceView(ctx, app) - k8sView, ok := (view).(*ManagedResourceView) + resourceView, ok := (view).(*ManagedResourceView) assert.Equal(t, ok, true) t.Run("init", func(t *testing.T) { - k8sView.Init() - assert.Equal(t, k8sView.Table.GetTitle(), "[ Managed Resource (all/all) ]") - assert.Equal(t, k8sView.GetCell(0, 0).Text, "Name") + resourceView.Init() + assert.Equal(t, resourceView.Table.GetTitle(), "[ Managed Resource (all/all) ]") + assert.Equal(t, resourceView.GetCell(0, 0).Text, "Name") }) t.Run("colorize text", func(t *testing.T) { @@ -66,26 +66,29 @@ func TestK8SView(t *testing.T) { {"app", "ns", "", "", "", "UnKnown"}} for i := 0; i < len(testData); i++ { for j := 0; j < len(testData[i]); j++ { - k8sView.Table.SetCell(1+i, j, tview.NewTableCell(testData[i][j])) + resourceView.Table.SetCell(1+i, j, tview.NewTableCell(testData[i][j])) } } - k8sView.ColorizeStatusText(4) - assert.Equal(t, k8sView.GetCell(1, 5).Text, "[green::]Healthy") - assert.Equal(t, k8sView.GetCell(2, 5).Text, "[red::]UnHealthy") - assert.Equal(t, k8sView.GetCell(3, 5).Text, "[blue::]Progressing") - assert.Equal(t, k8sView.GetCell(4, 5).Text, "[gray::]UnKnown") + resourceView.ColorizeStatusText(4) + assert.Equal(t, resourceView.GetCell(1, 5).Text, "[green::]Healthy") + assert.Equal(t, resourceView.GetCell(2, 5).Text, "[red::]UnHealthy") + assert.Equal(t, resourceView.GetCell(3, 5).Text, "[blue::]Progressing") + assert.Equal(t, resourceView.GetCell(4, 5).Text, "[gray::]UnKnown") }) t.Run("hint", func(t *testing.T) { - assert.Equal(t, len(k8sView.Hint()), 4) + assert.Equal(t, len(resourceView.Hint()), 5) }) t.Run("select cluster", func(t *testing.T) { - assert.Empty(t, k8sView.clusterView(nil)) + assert.Empty(t, resourceView.clusterView(nil)) }) t.Run("select cluster namespace", func(t *testing.T) { - assert.Empty(t, k8sView.clusterNamespaceView(nil)) + assert.Empty(t, resourceView.clusterNamespaceView(nil)) }) + t.Run("pod view", func(t *testing.T) { + assert.Empty(t, resourceView.podView(nil)) + }) } diff --git a/references/cli/top/view/pod_view.go b/references/cli/top/view/pod_view.go new file mode 100644 index 000000000..be089f91d --- /dev/null +++ b/references/cli/top/view/pod_view.go @@ -0,0 +1,104 @@ +/* +Copyright 2022 The KubeVela Authors. + +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 view + +import ( + "context" + "fmt" + "log" + + "github.com/gdamore/tcell/v2" + v1 "k8s.io/api/core/v1" + + "github.com/oam-dev/kubevela/references/cli/top/component" + "github.com/oam-dev/kubevela/references/cli/top/config" + "github.com/oam-dev/kubevela/references/cli/top/model" +) + +// PodView is the pod view, this view display info of pod belonging to component +type PodView struct { + *ResourceView + ctx context.Context +} + +// NewPodView return a new pod view +func NewPodView(ctx context.Context, app *App) model.Component { + v := &PodView{ + ResourceView: NewResourceView(app), + ctx: ctx, + } + return v +} + +// Init cluster view init +func (v *PodView) Init() { + // set title of view + title := fmt.Sprintf("[ %s ]", v.Name()) + v.SetTitle(title).SetTitleColor(config.ResourceTableTitleColor) + + resourceList := v.ListPods() + v.ResourceView.Init(resourceList) + v.ColorizePhaseText(len(resourceList.Body())) + + v.bindKeys() +} + +// ListPods list pods of component +func (v *PodView) ListPods() model.ResourceList { + list, err := model.ListPods(v.ctx, v.app.config.RestConfig, v.app.client) + if err != nil { + log.Println(err) + } + return list +} + +// ColorizePhaseText colorize the phase column text +func (v *PodView) ColorizePhaseText(rowNum int) { + for i := 1; i < rowNum+1; i++ { + phase := v.Table.GetCell(i, 3).Text + switch v1.PodPhase(phase) { + case v1.PodPending: + phase = config.PodPendingPhaseColor + phase + case v1.PodRunning: + phase = config.PodRunningPhaseColor + phase + case v1.PodSucceeded: + phase = config.PodSucceededPhase + phase + case v1.PodFailed: + phase = config.PodFailedPhase + phase + default: + } + v.Table.GetCell(i, 3).SetText(phase) + } +} + +// Name return pod view name +func (v *PodView) Name() string { + return "Pod" +} + +// Hint return key action menu hints of the pod view +func (v *PodView) Hint() []model.MenuHint { + return v.Actions().Hint() +} + +func (v *PodView) bindKeys() { + v.Actions().Delete([]tcell.Key{tcell.KeyEnter}) + v.Actions().Add(model.KeyActions{ + tcell.KeyESC: model.KeyAction{Description: "Back", Action: v.app.Back, Visible: true, Shared: true}, + component.KeyHelp: model.KeyAction{Description: "Help", Action: v.app.helpView, Visible: true, Shared: true}, + }) +} diff --git a/references/cli/top/view/pod_view_test.go b/references/cli/top/view/pod_view_test.go new file mode 100644 index 000000000..55cadaa18 --- /dev/null +++ b/references/cli/top/view/pod_view_test.go @@ -0,0 +1,85 @@ +/* +Copyright 2022 The KubeVela Authors. + +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 view + +import ( + "context" + "testing" + "time" + + "github.com/rivo/tview" + + "github.com/stretchr/testify/assert" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + + "github.com/oam-dev/kubevela/pkg/utils/common" + "github.com/oam-dev/kubevela/references/cli/top/model" +) + +func TestPodView(t *testing.T) { + testEnv := &envtest.Environment{ + ControlPlaneStartTimeout: time.Minute * 3, + ControlPlaneStopTimeout: time.Minute, + UseExistingCluster: pointer.BoolPtr(false), + } + cfg, err := testEnv.Start() + assert.NoError(t, err) + testClient, err := client.New(cfg, client.Options{Scheme: common.Scheme}) + assert.NoError(t, err) + app := NewApp(testClient, cfg, "") + assert.Equal(t, len(app.Components()), 4) + ctx := context.Background() + ctx = context.WithValue(ctx, &model.CtxKeyAppName, "") + ctx = context.WithValue(ctx, &model.CtxKeyNamespace, "") + ctx = context.WithValue(ctx, &model.CtxKeyCluster, "") + ctx = context.WithValue(ctx, &model.CtxKeyClusterNamespace, "") + ctx = context.WithValue(ctx, &model.CtxKeyComponentName, "") + + view, ok := NewPodView(ctx, app).(*PodView) + assert.Equal(t, ok, true) + + t.Run("init", func(t *testing.T) { + view.Init() + assert.Equal(t, view.Table.GetTitle(), "[ Pod ]") + assert.Equal(t, view.GetCell(0, 0).Text, "Name") + }) + + t.Run("colorize text", func(t *testing.T) { + testData := [][]string{ + {"app", "ns", "1/1", "Running", "", "", "", "", "", "", "", "", ""}, + {"app", "ns", "1/1", "Pending", "", "", "", "", "", "", "", "", ""}, + {"app", "ns", "1/1", "Succeeded", "", "", "", "", "", "", "", "", ""}, + {"app", "ns", "1/1", "Failed", "", "", "", "", "", "", "", "", ""}, + } + for i := 0; i < len(testData); i++ { + for j := 0; j < len(testData[i]); j++ { + view.Table.SetCell(1+i, j, tview.NewTableCell(testData[i][j])) + } + } + view.ColorizePhaseText(5) + assert.Equal(t, view.GetCell(1, 3).Text, "[green::]Running") + assert.Equal(t, view.GetCell(2, 3).Text, "[yellow::]Pending") + assert.Equal(t, view.GetCell(3, 3).Text, "[purple::]Succeeded") + assert.Equal(t, view.GetCell(4, 3).Text, "[red::]Failed") + }) + + t.Run("hint", func(t *testing.T) { + assert.Equal(t, len(view.Hint()), 2) + }) +} diff --git a/references/cli/top/view/resource_view.go b/references/cli/top/view/resource_view.go index 2ecf139a7..7da93a9f8 100644 --- a/references/cli/top/view/resource_view.go +++ b/references/cli/top/view/resource_view.go @@ -53,6 +53,9 @@ var ResourceMap = map[string]ResourceViewer{ "cns": { viewFunc: NewClusterNamespaceView, }, + "pod": { + viewFunc: NewPodView, + }, } // NewResourceView return a new resource view