diff --git a/cmd/api/app/api.go b/cmd/api/app/api.go index 76af08c0..7cd29e76 100644 --- a/cmd/api/app/api.go +++ b/cmd/api/app/api.go @@ -26,7 +26,7 @@ import ( _ "github.com/karmada-io/dashboard/cmd/api/app/routes/deployment" _ "github.com/karmada-io/dashboard/cmd/api/app/routes/ingress" _ "github.com/karmada-io/dashboard/cmd/api/app/routes/job" - _ "github.com/karmada-io/dashboard/cmd/api/app/routes/member/node" + _ "github.com/karmada-io/dashboard/cmd/api/app/routes/member" _ "github.com/karmada-io/dashboard/cmd/api/app/routes/namespace" _ "github.com/karmada-io/dashboard/cmd/api/app/routes/overridepolicy" _ "github.com/karmada-io/dashboard/cmd/api/app/routes/overview" diff --git a/cmd/api/app/router/middleware.go b/cmd/api/app/router/middleware.go new file mode 100644 index 00000000..769752b5 --- /dev/null +++ b/cmd/api/app/router/middleware.go @@ -0,0 +1,25 @@ +package router + +import ( + "context" + "github.com/gin-gonic/gin" + "github.com/karmada-io/dashboard/cmd/api/app/types/common" + "github.com/karmada-io/dashboard/pkg/client" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "net/http" +) + +func EnsureMemberClusterMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + karmadaClient := client.InClusterKarmadaClient() + _, err := karmadaClient.ClusterV1alpha1().Clusters().Get(context.TODO(), c.Param("clustername"), metav1.GetOptions{}) + if err != nil { + c.AbortWithStatusJSON(http.StatusOK, common.BaseResponse{ + Code: 500, + Msg: err.Error(), + }) + return + } + c.Next() + } +} diff --git a/cmd/api/app/router/setup.go b/cmd/api/app/router/setup.go index 045f7c42..45156a10 100644 --- a/cmd/api/app/router/setup.go +++ b/cmd/api/app/router/setup.go @@ -8,6 +8,7 @@ import ( var ( router *gin.Engine v1 *gin.RouterGroup + member *gin.RouterGroup ) func init() { @@ -18,6 +19,8 @@ func init() { router = gin.Default() _ = router.SetTrustedProxies(nil) v1 = router.Group("/api/v1") + member = v1.Group("/member/:clustername") + member.Use(EnsureMemberClusterMiddleware()) router.GET("/livez", func(c *gin.Context) { c.String(200, "livez") @@ -34,3 +37,7 @@ func V1() *gin.RouterGroup { func Router() *gin.Engine { return router } + +func MemberV1() *gin.RouterGroup { + return member +} diff --git a/cmd/api/app/routes/member/deployment/handler.go b/cmd/api/app/routes/member/deployment/handler.go new file mode 100644 index 00000000..3e531e6a --- /dev/null +++ b/cmd/api/app/routes/member/deployment/handler.go @@ -0,0 +1,55 @@ +package deployment + +import ( + "github.com/gin-gonic/gin" + "github.com/karmada-io/dashboard/cmd/api/app/router" + "github.com/karmada-io/dashboard/cmd/api/app/types/common" + "github.com/karmada-io/dashboard/pkg/client" + "github.com/karmada-io/dashboard/pkg/resource/deployment" + "github.com/karmada-io/dashboard/pkg/resource/event" +) + +func handleGetMemberDeployments(c *gin.Context) { + memberClient := client.InClusterClientForMemberCluster(c.Param("clustername")) + namespace := common.ParseNamespacePathParameter(c) + dataSelect := common.ParseDataSelectPathParameter(c) + result, err := deployment.GetDeploymentList(memberClient, namespace, dataSelect) + if err != nil { + common.Fail(c, err) + return + } + common.Success(c, result) +} + +func handleGetMemberDeploymentDetail(c *gin.Context) { + memberClient := client.InClusterClientForMemberCluster(c.Param("clustername")) + namespace := c.Param("namespace") + name := c.Param("deployment") + result, err := deployment.GetDeploymentDetail(memberClient, namespace, name) + if err != nil { + common.Fail(c, err) + return + } + common.Success(c, result) +} + +func handleGetMemberDeploymentEvents(c *gin.Context) { + memberClient := client.InClusterClientForMemberCluster(c.Param("clustername")) + namespace := c.Param("namespace") + name := c.Param("deployment") + dataSelect := common.ParseDataSelectPathParameter(c) + result, err := event.GetResourceEvents(memberClient, dataSelect, namespace, name) + if err != nil { + common.Fail(c, err) + return + } + common.Success(c, result) +} + +func init() { + r := router.MemberV1() + r.GET("/deployment", handleGetMemberDeployments) + r.GET("/deployment/:namespace", handleGetMemberDeployments) + r.GET("/deployment/:namespace/:deployment", handleGetMemberDeploymentDetail) + r.GET("/deployment/:namespace/:deployment/event", handleGetMemberDeploymentEvents) +} diff --git a/cmd/api/app/routes/member/member.go b/cmd/api/app/routes/member/member.go new file mode 100644 index 00000000..96fa644d --- /dev/null +++ b/cmd/api/app/routes/member/member.go @@ -0,0 +1,9 @@ +package member + +// Importing member route packages forces route registration +import ( + _ "github.com/karmada-io/dashboard/cmd/api/app/routes/member/deployment" + _ "github.com/karmada-io/dashboard/cmd/api/app/routes/member/namespace" + _ "github.com/karmada-io/dashboard/cmd/api/app/routes/member/node" + _ "github.com/karmada-io/dashboard/cmd/api/app/routes/member/pod" +) diff --git a/cmd/api/app/routes/member/namespace/handler.go b/cmd/api/app/routes/member/namespace/handler.go new file mode 100644 index 00000000..1abcc618 --- /dev/null +++ b/cmd/api/app/routes/member/namespace/handler.go @@ -0,0 +1,54 @@ +package namespace + +import ( + "github.com/gin-gonic/gin" + "github.com/karmada-io/dashboard/cmd/api/app/router" + "github.com/karmada-io/dashboard/cmd/api/app/types/common" + "github.com/karmada-io/dashboard/pkg/client" + "github.com/karmada-io/dashboard/pkg/resource/event" + ns "github.com/karmada-io/dashboard/pkg/resource/namespace" +) + +func handleGetMemberNamespace(c *gin.Context) { + memberClient := client.InClusterClientForMemberCluster(c.Param("clustername")) + + dataSelect := common.ParseDataSelectPathParameter(c) + result, err := ns.GetNamespaceList(memberClient, dataSelect) + if err != nil { + common.Fail(c, err) + return + } + common.Success(c, result) +} + +func handleGetMemberNamespaceDetail(c *gin.Context) { + memberClient := client.InClusterClientForMemberCluster(c.Param("clustername")) + + name := c.Param("name") + result, err := ns.GetNamespaceDetail(memberClient, name) + if err != nil { + common.Fail(c, err) + return + } + common.Success(c, result) +} + +func handleGetMemberNamespaceEvents(c *gin.Context) { + memberClient := client.InClusterClientForMemberCluster(c.Param("clustername")) + + name := c.Param("name") + dataSelect := common.ParseDataSelectPathParameter(c) + result, err := event.GetNamespaceEvents(memberClient, dataSelect, name) + if err != nil { + common.Fail(c, err) + return + } + common.Success(c, result) +} + +func init() { + r := router.MemberV1() + r.GET("/namespace", handleGetMemberNamespace) + r.GET("/namespace/:name", handleGetMemberNamespaceDetail) + r.GET("/namespace/:name/event", handleGetMemberNamespaceEvents) +} diff --git a/cmd/api/app/routes/member/node/handler.go b/cmd/api/app/routes/member/node/handler.go index 550d82a0..68055e14 100644 --- a/cmd/api/app/routes/member/node/handler.go +++ b/cmd/api/app/routes/member/node/handler.go @@ -1,22 +1,14 @@ package node import ( - "context" "github.com/gin-gonic/gin" "github.com/karmada-io/dashboard/cmd/api/app/router" "github.com/karmada-io/dashboard/cmd/api/app/types/common" "github.com/karmada-io/dashboard/pkg/client" "github.com/karmada-io/dashboard/pkg/resource/node" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func handleGetClusterNode(c *gin.Context) { - karmadaClient := client.InClusterKarmadaClient() - _, err := karmadaClient.ClusterV1alpha1().Clusters().Get(context.TODO(), c.Param("clustername"), metav1.GetOptions{}) - if err != nil { - common.Fail(c, err) - return - } memberClient := client.InClusterClientForMemberCluster(c.Param("clustername")) dataSelect := common.ParseDataSelectPathParameter(c) result, err := node.GetNodeList(memberClient, dataSelect) @@ -28,6 +20,6 @@ func handleGetClusterNode(c *gin.Context) { } func init() { - r := router.V1() - r.GET("/member/:clustername/node", handleGetClusterNode) + r := router.MemberV1() + r.GET("/node", handleGetClusterNode) } diff --git a/cmd/api/app/routes/member/pod/handler.go b/cmd/api/app/routes/member/pod/handler.go new file mode 100644 index 00000000..f55621dd --- /dev/null +++ b/cmd/api/app/routes/member/pod/handler.go @@ -0,0 +1,42 @@ +package pod + +import ( + "github.com/gin-gonic/gin" + "github.com/karmada-io/dashboard/cmd/api/app/router" + "github.com/karmada-io/dashboard/cmd/api/app/types/common" + "github.com/karmada-io/dashboard/pkg/client" + "github.com/karmada-io/dashboard/pkg/resource/pod" +) + +// return a pods list +func handleGetMemberPod(c *gin.Context) { + memberClient := client.InClusterClientForMemberCluster(c.Param("clustername")) + dataSelect := common.ParseDataSelectPathParameter(c) + nsQuery := common.ParseNamespacePathParameter(c) + result, err := pod.GetPodList(memberClient, nsQuery, dataSelect) + if err != nil { + common.Fail(c, err) + return + } + common.Success(c, result) +} + +// return a pod detail +func handleGetMemberPodDetail(c *gin.Context) { + memberClient := client.InClusterClientForMemberCluster(c.Param("clustername")) + namespace := c.Param("namespace") + name := c.Param("name") + result, err := pod.GetPodDetail(memberClient, namespace, name) + if err != nil { + common.Fail(c, err) + return + } + common.Success(c, result) +} + +func init() { + r := router.MemberV1() + r.GET("/pod", handleGetMemberPod) + r.GET("/pod/:namespace", handleGetMemberPod) + r.GET("/pod/:namespace/:name", handleGetMemberPodDetail) +} diff --git a/pkg/resource/pod/common.go b/pkg/resource/pod/common.go new file mode 100644 index 00000000..cc19c90e --- /dev/null +++ b/pkg/resource/pod/common.go @@ -0,0 +1,38 @@ +package pod + +import ( + "github.com/karmada-io/dashboard/pkg/dataselect" + api "k8s.io/api/core/v1" +) + +type PodCell api.Pod + +func (self PodCell) GetProperty(name dataselect.PropertyName) dataselect.ComparableValue { + switch name { + case dataselect.NameProperty: + return dataselect.StdComparableString(self.ObjectMeta.Name) + case dataselect.CreationTimestampProperty: + return dataselect.StdComparableTime(self.ObjectMeta.CreationTimestamp.Time) + case dataselect.NamespaceProperty: + return dataselect.StdComparableString(self.ObjectMeta.Namespace) + default: + // if name is not supported then just return a constant dummy value, sort will have no effect. + return nil + } +} + +func toCells(std []api.Pod) []dataselect.DataCell { + cells := make([]dataselect.DataCell, len(std)) + for i := range std { + cells[i] = PodCell(std[i]) + } + return cells +} + +func fromCells(cells []dataselect.DataCell) []api.Pod { + std := make([]api.Pod, len(cells)) + for i := range std { + std[i] = api.Pod(cells[i].(PodCell)) + } + return std +} diff --git a/pkg/resource/pod/detail.go b/pkg/resource/pod/detail.go new file mode 100644 index 00000000..112389f1 --- /dev/null +++ b/pkg/resource/pod/detail.go @@ -0,0 +1,25 @@ +package pod + +import ( + "context" + + v1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +type PodDeatil struct { + ObjectMeta metaV1.ObjectMeta `json:"objectMeta"` + TypeMeta metaV1.TypeMeta `json:"typeMeta"` + Spec v1.PodSpec `json:"podSpec"` + Status v1.PodStatus `json:"status"` +} + +// GetPodDetail returns a Pod detail +func GetPodDetail(client kubernetes.Interface, namespace, name string) (*v1.Pod, error) { + podData, err := client.CoreV1().Pods(namespace).Get(context.TODO(), name, metaV1.GetOptions{}) + if err != nil { + return nil, err + } + return podData, nil +} diff --git a/pkg/resource/pod/list.go b/pkg/resource/pod/list.go new file mode 100644 index 00000000..352433a2 --- /dev/null +++ b/pkg/resource/pod/list.go @@ -0,0 +1,86 @@ +package pod + +import ( + "log" + + "github.com/karmada-io/dashboard/pkg/common/errors" + "github.com/karmada-io/dashboard/pkg/common/types" + "github.com/karmada-io/dashboard/pkg/dataselect" + "github.com/karmada-io/dashboard/pkg/resource/common" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +type Pod struct { + ObjectMeta types.ObjectMeta `json:"objectMeta"` + TypeMeta types.TypeMeta `json:"typeMeta"` + Status v1.PodStatus `json:"status"` +} + +// PodList contains a list of pod. +type PodList struct { + ListMeta types.ListMeta `json:"listMeta"` + + // Unordered list of Pods + Items []Pod `json:"items"` + + // List of non-critical errors, that occurred during resource retrieval. + Errors []error `json:"errors"` +} + +// GetPodList returns a list of all Pods in all cluster. +func GetPodList(client kubernetes.Interface, nsQuery *common.NamespaceQuery, dsQuery *dataselect.DataSelectQuery) (*PodList, error) { + log.Printf("Getting pods") + channels := &common.ResourceChannels{ + PodList: common.GetPodListChannel(client, nsQuery, 1), + } + + return GetPodListFromChannels(channels, dsQuery) +} + +// GetPodListFromChannels returns a list of all Pods in the cluster reading required resource list once from the channels. +func GetPodListFromChannels(channels *common.ResourceChannels, dsQuery *dataselect.DataSelectQuery) (*PodList, error) { + pods := <-channels.PodList.List + err := <-channels.PodList.Error + nonCriticalErrors, criticalError := errors.ExtractErrors(err) + if criticalError != nil { + return nil, criticalError + } + + result := toPodList(pods.Items, nonCriticalErrors, dsQuery) + + return result, nil +} + +func toPod(meta metav1.ObjectMeta, status v1.PodStatus) Pod { + return Pod{ + ObjectMeta: types.NewObjectMeta(meta), + TypeMeta: types.NewTypeMeta(types.ResourceKindPod), + Status: NewStatus(status), + } +} + +func toPodList(pods []v1.Pod, nonCriticalErrors []error, dsQuery *dataselect.DataSelectQuery) *PodList { + result := &PodList{ + Items: make([]Pod, 0), + ListMeta: types.ListMeta{TotalItems: len(pods)}, + Errors: nonCriticalErrors, + } + + podCells, filteredTotal := dataselect.GenericDataSelectWithFilter(toCells(pods), dsQuery) + pods = fromCells(podCells) + result.ListMeta = types.ListMeta{TotalItems: filteredTotal} + + for _, item := range pods { + result.Items = append(result.Items, toPod(item.ObjectMeta, item.Status)) + } + + return result +} + +func NewStatus(status v1.PodStatus) v1.PodStatus { + return v1.PodStatus{ + Conditions: status.Conditions, + } +}