Skip to content

Commit

Permalink
implement statefulset resources (#65)
Browse files Browse the repository at this point in the history
* feat: not allow wrap for small device

Signed-off-by: warjiang <[email protected]>

* feat: api for statefulset

Signed-off-by: warjiang <[email protected]>

* feat: opt label view for workloads

Signed-off-by: warjiang <[email protected]>

* feat: add search filter for workload; add statefulset implementation

Signed-off-by: warjiang <[email protected]>

---------

Signed-off-by: warjiang <[email protected]>
  • Loading branch information
warjiang authored Aug 15, 2024
1 parent 6f501fd commit 8777d59
Show file tree
Hide file tree
Showing 12 changed files with 501 additions and 43 deletions.
1 change: 1 addition & 0 deletions cmd/api/app/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
_ "github.com/karmada-io/dashboard/cmd/api/app/routes/namespace"
_ "github.com/karmada-io/dashboard/cmd/api/app/routes/overview"
_ "github.com/karmada-io/dashboard/cmd/api/app/routes/propagationpolicy"
_ "github.com/karmada-io/dashboard/cmd/api/app/routes/statefulset"
_ "github.com/karmada-io/dashboard/cmd/api/app/routes/unstructured"
)

Expand Down
54 changes: 54 additions & 0 deletions cmd/api/app/routes/statefulset/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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/event"
"github.com/karmada-io/dashboard/pkg/resource/statefulset"
)

func handleGetStatefulsets(c *gin.Context) {
namespace := common.ParseNamespacePathParameter(c)
dataSelect := common.ParseDataSelectPathParameter(c)
k8sClient := client.InClusterClientForKarmadaApiServer()
result, err := statefulset.GetStatefulSetList(k8sClient, namespace, dataSelect)
if err != nil {
common.Fail(c, err)
return
}
common.Success(c, result)
}

func handleGetStatefulsetDetail(c *gin.Context) {
namespace := c.Param("namespace")
name := c.Param("statefulset")
k8sClient := client.InClusterClientForKarmadaApiServer()
result, err := statefulset.GetStatefulSetDetail(k8sClient, namespace, name)
if err != nil {
common.Fail(c, err)
return
}
common.Success(c, result)
}

func handleGetStatefulsetEvents(c *gin.Context) {
namespace := c.Param("namespace")
name := c.Param("statefulset")
k8sClient := client.InClusterClientForKarmadaApiServer()
dataSelect := common.ParseDataSelectPathParameter(c)
result, err := event.GetResourceEvents(k8sClient, dataSelect, namespace, name)
if err != nil {
common.Fail(c, err)
return
}
common.Success(c, result)
}
func init() {
r := router.V1()
r.GET("/statefulset", handleGetStatefulsets)
r.GET("/statefulset/:namespace", handleGetStatefulsets)
r.GET("/statefulset/:namespace/:statefulset", handleGetStatefulsetDetail)
r.GET("/statefulset/:namespace/:statefulset/event", handleGetStatefulsetEvents)
}
27 changes: 27 additions & 0 deletions pkg/resource/common/resourcechannels.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,33 @@ type StatefulSetListChannel struct {
Error chan error
}

// GetStatefulSetListChannel returns a pair of channels to a StatefulSet list and errors that both must be read
// numReads times.
func GetStatefulSetListChannel(client client.Interface,
nsQuery *NamespaceQuery, numReads int) StatefulSetListChannel {
channel := StatefulSetListChannel{
List: make(chan *apps.StatefulSetList, numReads),
Error: make(chan error, numReads),
}

go func() {
statefulSets, err := client.AppsV1().StatefulSets(nsQuery.ToRequestParam()).List(context.TODO(), helpers.ListEverything)
var filteredItems []apps.StatefulSet
for _, item := range statefulSets.Items {
if nsQuery.Matches(item.ObjectMeta.Namespace) {
filteredItems = append(filteredItems, item)
}
}
statefulSets.Items = filteredItems
for i := 0; i < numReads; i++ {
channel.List <- statefulSets
channel.Error <- err
}
}()

return channel
}

// ConfigMapListChannel is a list and error channels to ConfigMaps.
type ConfigMapListChannel struct {
List chan *v1.ConfigMapList
Expand Down
66 changes: 66 additions & 0 deletions pkg/resource/statefulset/common.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package statefulset

import (
"github.com/karmada-io/dashboard/pkg/dataselect"
"github.com/karmada-io/dashboard/pkg/resource/common"
"github.com/karmada-io/dashboard/pkg/resource/event"
apps "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
)

// The code below allows to perform complex data section on []apps.StatefulSet

type StatefulSetCell apps.StatefulSet

func (self StatefulSetCell) 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 []apps.StatefulSet) []dataselect.DataCell {
cells := make([]dataselect.DataCell, len(std))
for i := range std {
cells[i] = StatefulSetCell(std[i])
}
return cells
}

func fromCells(cells []dataselect.DataCell) []apps.StatefulSet {
std := make([]apps.StatefulSet, len(cells))
for i := range std {
std[i] = apps.StatefulSet(cells[i].(StatefulSetCell))
}
return std
}

func getStatus(list *apps.StatefulSetList, pods []v1.Pod, events []v1.Event) common.ResourceStatus {
info := common.ResourceStatus{}
if list == nil {
return info
}

for _, ss := range list.Items {
matchingPods := common.FilterPodsByControllerRef(&ss, pods)
podInfo := common.GetPodInfo(ss.Status.Replicas, ss.Spec.Replicas, matchingPods)
warnings := event.GetPodsEventWarnings(events, matchingPods)

if len(warnings) > 0 {
info.Failed++
} else if podInfo.Pending > 0 {
info.Pending++
} else {
info.Running++
}
}

return info
}
47 changes: 47 additions & 0 deletions pkg/resource/statefulset/detail.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package statefulset

import (
"context"
"github.com/karmada-io/dashboard/pkg/common/errors"
"github.com/karmada-io/dashboard/pkg/resource/common"
apps "k8s.io/api/apps/v1"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"log"
)

// StatefulSetDetail is a presentation layer view of Kubernetes Stateful Set resource. This means it is Stateful
type StatefulSetDetail struct {
// Extends list item structure.
StatefulSet `json:",inline"`

// List of non-critical errors, that occurred during resource retrieval.
Errors []error `json:"errors"`
}

// GetStatefulSetDetail gets Stateful Set details.
func GetStatefulSetDetail(client kubernetes.Interface, namespace,
name string) (*StatefulSetDetail, error) {
log.Printf("Getting details of %s statefulset in %s namespace", name, namespace)

ss, err := client.AppsV1().StatefulSets(namespace).Get(context.TODO(), name, metaV1.GetOptions{})
if err != nil {
return nil, err
}

podInfo, err := getStatefulSetPodInfo(client, ss)
nonCriticalErrors, criticalError := errors.ExtractErrors(err)
if criticalError != nil {
return nil, criticalError
}

ssDetail := getStatefulSetDetail(ss, podInfo, nonCriticalErrors)
return &ssDetail, nil
}

func getStatefulSetDetail(statefulSet *apps.StatefulSet, podInfo *common.PodInfo, nonCriticalErrors []error) StatefulSetDetail {
return StatefulSetDetail{
StatefulSet: toStatefulSet(statefulSet, podInfo),
Errors: nonCriticalErrors,
}
}
110 changes: 110 additions & 0 deletions pkg/resource/statefulset/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package statefulset

import (
"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"
"github.com/karmada-io/dashboard/pkg/resource/event"
apps "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/client-go/kubernetes"
"log"
)

// StatefulSetList contains a list of Stateful Sets in the cluster.
type StatefulSetList struct {
ListMeta types.ListMeta `json:"listMeta"`

Status common.ResourceStatus `json:"status"`
StatefulSets []StatefulSet `json:"statefulSets"`

// List of non-critical errors, that occurred during resource retrieval.
Errors []error `json:"errors"`
}

// StatefulSet is a presentation layer view of Kubernetes Stateful Set resource.
type StatefulSet struct {
ObjectMeta types.ObjectMeta `json:"objectMeta"`
TypeMeta types.TypeMeta `json:"typeMeta"`
Pods common.PodInfo `json:"podInfo"`
ContainerImages []string `json:"containerImages"`
InitContainerImages []string `json:"initContainerImages"`
}

// GetStatefulSetList returns a list of all Stateful Sets in the cluster.
func GetStatefulSetList(client kubernetes.Interface, nsQuery *common.NamespaceQuery,
dsQuery *dataselect.DataSelectQuery) (*StatefulSetList, error) {
log.Print("Getting list of all stateful sets in the cluster")

channels := &common.ResourceChannels{
StatefulSetList: common.GetStatefulSetListChannel(client, nsQuery, 1),
PodList: common.GetPodListChannel(client, nsQuery, 1),
EventList: common.GetEventListChannel(client, nsQuery, 1),
}

return GetStatefulSetListFromChannels(channels, dsQuery)
}

// GetStatefulSetListFromChannels returns a list of all Stateful Sets in the cluster reading
// required resource list once from the channels.
func GetStatefulSetListFromChannels(channels *common.ResourceChannels, dsQuery *dataselect.DataSelectQuery) (*StatefulSetList, error) {

statefulSets := <-channels.StatefulSetList.List
err := <-channels.StatefulSetList.Error
nonCriticalErrors, criticalError := errors.ExtractErrors(err)
if criticalError != nil {
return nil, criticalError
}

pods := <-channels.PodList.List
err = <-channels.PodList.Error
nonCriticalErrors, criticalError = errors.AppendError(err, nonCriticalErrors)
if criticalError != nil {
return nil, criticalError
}

events := <-channels.EventList.List
err = <-channels.EventList.Error
nonCriticalErrors, criticalError = errors.AppendError(err, nonCriticalErrors)
if criticalError != nil {
return nil, criticalError
}

ssList := toStatefulSetList(statefulSets.Items, pods.Items, events.Items, nonCriticalErrors, dsQuery)
ssList.Status = getStatus(statefulSets, pods.Items, events.Items)
return ssList, nil
}

func toStatefulSetList(statefulSets []apps.StatefulSet, pods []v1.Pod, events []v1.Event, nonCriticalErrors []error,
dsQuery *dataselect.DataSelectQuery) *StatefulSetList {

statefulSetList := &StatefulSetList{
StatefulSets: make([]StatefulSet, 0),
ListMeta: types.ListMeta{TotalItems: len(statefulSets)},
Errors: nonCriticalErrors,
}

ssCells, filteredTotal := dataselect.GenericDataSelectWithFilter(toCells(statefulSets), dsQuery)
statefulSets = fromCells(ssCells)
statefulSetList.ListMeta = types.ListMeta{TotalItems: filteredTotal}

for _, statefulSet := range statefulSets {
matchingPods := common.FilterPodsByControllerRef(&statefulSet, pods)
podInfo := common.GetPodInfo(statefulSet.Status.Replicas, statefulSet.Spec.Replicas, matchingPods)
podInfo.Warnings = event.GetPodsEventWarnings(events, matchingPods)
statefulSetList.StatefulSets = append(statefulSetList.StatefulSets, toStatefulSet(&statefulSet, &podInfo))
}

return statefulSetList
}

func toStatefulSet(statefulSet *apps.StatefulSet, podInfo *common.PodInfo) StatefulSet {
return StatefulSet{
ObjectMeta: types.NewObjectMeta(statefulSet.ObjectMeta),
TypeMeta: types.NewTypeMeta(types.ResourceKindStatefulSet),
ContainerImages: common.GetContainerImages(&statefulSet.Spec.Template.Spec),
InitContainerImages: common.GetInitContainerImages(&statefulSet.Spec.Template.Spec),
Pods: *podInfo,
}
}
40 changes: 40 additions & 0 deletions pkg/resource/statefulset/pods.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package statefulset

import (
"context"
"github.com/karmada-io/dashboard/pkg/resource/common"
apps "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)

// getRawStatefulSetPods return array of api pods targeting pet set with given name.
func getRawStatefulSetPods(client kubernetes.Interface, name, namespace string) ([]v1.Pod, error) {
statefulSet, err := client.AppsV1().StatefulSets(namespace).Get(context.TODO(), name, metaV1.GetOptions{})
if err != nil {
return nil, err
}

channels := &common.ResourceChannels{
PodList: common.GetPodListChannel(client, common.NewSameNamespaceQuery(namespace), 1),
}

podList := <-channels.PodList.List
if err := <-channels.PodList.Error; err != nil {
return nil, err
}

return common.FilterPodsByControllerRef(statefulSet, podList.Items), nil
}

// Returns simple info about pods(running, desired, failing, etc.) related to given pet set.
func getStatefulSetPodInfo(client kubernetes.Interface, statefulSet *apps.StatefulSet) (*common.PodInfo, error) {
pods, err := getRawStatefulSetPods(client, statefulSet.Name, statefulSet.Namespace)
if err != nil {
return nil, err
}

podInfo := common.GetPodInfo(statefulSet.Status.Replicas, statefulSet.Spec.Replicas, pods)
return &podInfo, nil
}
15 changes: 9 additions & 6 deletions ui/apps/dashboard/src/components/tag-list/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FC } from 'react';
import { Dropdown, Tag } from 'antd';
import { cn } from '@/utils/cn.ts';

interface ITagListProps {
tags: {
Expand All @@ -18,12 +19,14 @@ const TagList: FC<ITagListProps> = (props) => {
) : tags.length <= maxLen ? (
tags.map((t) => <Tag key={t.key}>{t.value}</Tag>)
) : (
<div className={'flex flex-row'}>
<div>
{tags.slice(0, maxLen).map((t) => (
<Tag key={t.key}>{t.value}</Tag>
))}
</div>
<div
className={cn('flex', 'flex-row', {
'flex-no-wrap': tags.length > maxLen,
})}
>
{tags.slice(0, maxLen).map((t) => (
<Tag key={t.key}>{t.value}</Tag>
))}
<Dropdown
menu={{
items: tags.slice(maxLen).map((t) => ({
Expand Down
Loading

0 comments on commit 8777d59

Please sign in to comment.