Feat: add a new resource level view: pod view (#4661)

Pod view is the next level view of managed resource view

Signed-off-by: HanMengnan <1448189829@qq.com>

Signed-off-by: HanMengnan <1448189829@qq.com>
This commit is contained in:
Siege Lion
2022-08-29 10:30:22 +08:00
committed by GitHub
parent a4ad1ad40f
commit 3531249e1b
19 changed files with 819 additions and 84 deletions
+8
View File
@@ -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::]"
)
@@ -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)),
})
}
@@ -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))
})
})
+3 -21
View File
@@ -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])
}
@@ -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() {
+142
View File
@@ -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
}
+87
View File
@@ -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"))
})
})
+53
View File
@@ -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
}
+73
View File
@@ -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)
}
+128 -33
View File
@@ -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"}},
+2
View File
@@ -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 (
+18
View File
@@ -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 {
+40
View File
@@ -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])
}
+33
View File
@@ -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")
}
@@ -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
}
@@ -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))
})
}
+104
View File
@@ -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},
})
}
+85
View File
@@ -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)
})
}
+3
View File
@@ -53,6 +53,9 @@ var ResourceMap = map[string]ResourceViewer{
"cns": {
viewFunc: NewClusterNamespaceView,
},
"pod": {
viewFunc: NewPodView,
},
}
// NewResourceView return a new resource view