From a44939251cbf9090390e59f728bd7f911638de26 Mon Sep 17 00:00:00 2001 From: warjiang <1096409085@qq.com> Date: Wed, 16 Oct 2024 11:50:47 +0800 Subject: [PATCH] feat: add api for ingress resource Signed-off-by: warjiang <1096409085@qq.com> --- cmd/api/app/api.go | 1 + cmd/api/app/routes/ingress/handler.go | 40 ++++++++++ pkg/resource/ingress/common.go | 40 ++++++++++ pkg/resource/ingress/detail.go | 47 ++++++++++++ pkg/resource/ingress/filter.go | 50 +++++++++++++ pkg/resource/ingress/list.go | 104 ++++++++++++++++++++++++++ 6 files changed, 282 insertions(+) create mode 100644 cmd/api/app/routes/ingress/handler.go create mode 100644 pkg/resource/ingress/common.go create mode 100644 pkg/resource/ingress/detail.go create mode 100644 pkg/resource/ingress/filter.go create mode 100644 pkg/resource/ingress/list.go diff --git a/cmd/api/app/api.go b/cmd/api/app/api.go index 1213b60..171a66e 100644 --- a/cmd/api/app/api.go +++ b/cmd/api/app/api.go @@ -22,6 +22,7 @@ import ( _ "github.com/karmada-io/dashboard/cmd/api/app/routes/cronjob" _ "github.com/karmada-io/dashboard/cmd/api/app/routes/daemonset" _ "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/namespace" _ "github.com/karmada-io/dashboard/cmd/api/app/routes/overview" diff --git a/cmd/api/app/routes/ingress/handler.go b/cmd/api/app/routes/ingress/handler.go new file mode 100644 index 0000000..4850424 --- /dev/null +++ b/cmd/api/app/routes/ingress/handler.go @@ -0,0 +1,40 @@ +package ingress + +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/ingress" +) + +func handleGetIngress(c *gin.Context) { + k8sClient := client.InClusterClientForKarmadaApiServer() + dataSelect := common.ParseDataSelectPathParameter(c) + nsQuery := common.ParseNamespacePathParameter(c) + result, err := ingress.GetIngressList(k8sClient, nsQuery, dataSelect) + if err != nil { + common.Fail(c, err) + return + } + common.Success(c, result) +} + +func handleGetIngressDetail(c *gin.Context) { + k8sClient := client.InClusterClientForKarmadaApiServer() + namespace := c.Param("namespace") + name := c.Param("service") + result, err := ingress.GetIngressDetail(k8sClient, namespace, name) + if err != nil { + common.Fail(c, err) + return + } + common.Success(c, result) +} + +func init() { + r := router.V1() + r.GET("/ingress", handleGetIngress) + r.GET("/ingress/:namespace", handleGetIngress) + r.GET("/ingress/:namespace/:service", handleGetIngressDetail) +} diff --git a/pkg/resource/ingress/common.go b/pkg/resource/ingress/common.go new file mode 100644 index 0000000..06c2743 --- /dev/null +++ b/pkg/resource/ingress/common.go @@ -0,0 +1,40 @@ +package ingress + +import ( + "github.com/karmada-io/dashboard/pkg/dataselect" + v1 "k8s.io/api/networking/v1" +) + +// The code below allows to perform complex data section on []extensions.Ingress + +type IngressCell v1.Ingress + +func (self IngressCell) 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 []v1.Ingress) []dataselect.DataCell { + cells := make([]dataselect.DataCell, len(std)) + for i := range std { + cells[i] = IngressCell(std[i]) + } + return cells +} + +func fromCells(cells []dataselect.DataCell) []v1.Ingress { + std := make([]v1.Ingress, len(cells)) + for i := range std { + std[i] = v1.Ingress(cells[i].(IngressCell)) + } + return std +} diff --git a/pkg/resource/ingress/detail.go b/pkg/resource/ingress/detail.go new file mode 100644 index 0000000..28737e4 --- /dev/null +++ b/pkg/resource/ingress/detail.go @@ -0,0 +1,47 @@ +package ingress + +import ( + "context" + "log" + + v1 "k8s.io/api/networking/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + client "k8s.io/client-go/kubernetes" +) + +// IngressDetail API resource provides mechanisms to inject containers with configuration data while keeping +// containers agnostic of Kubernetes +type IngressDetail struct { + // Extends list item structure. + Ingress `json:",inline"` + + // Spec is the desired state of the Ingress. + Spec v1.IngressSpec `json:"spec"` + + // Status is the current state of the Ingress. + Status v1.IngressStatus `json:"status"` + + // List of non-critical errors, that occurred during resource retrieval. + Errors []error `json:"errors"` +} + +// GetIngressDetail returns detailed information about an ingress +func GetIngressDetail(client client.Interface, namespace, name string) (*IngressDetail, error) { + log.Printf("Getting details of %s ingress in %s namespace", name, namespace) + + rawIngress, err := client.NetworkingV1().Ingresses(namespace).Get(context.TODO(), name, metaV1.GetOptions{}) + + if err != nil { + return nil, err + } + + return getIngressDetail(rawIngress), nil +} + +func getIngressDetail(i *v1.Ingress) *IngressDetail { + return &IngressDetail{ + Ingress: toIngress(i), + Spec: i.Spec, + Status: i.Status, + } +} diff --git a/pkg/resource/ingress/filter.go b/pkg/resource/ingress/filter.go new file mode 100644 index 0000000..656aa0f --- /dev/null +++ b/pkg/resource/ingress/filter.go @@ -0,0 +1,50 @@ +package ingress + +import ( + "github.com/karmada-io/dashboard/pkg/common/types" + networkingv1 "k8s.io/api/networking/v1" +) + +func FilterIngressByService(ingresses []networkingv1.Ingress, serviceName string) []networkingv1.Ingress { + var matchingIngresses []networkingv1.Ingress + for _, ingress := range ingresses { + if ingressMatchesServiceName(ingress, serviceName) { + matchingIngresses = append(matchingIngresses, ingress) + } + } + return matchingIngresses +} + +func ingressMatchesServiceName(ingress networkingv1.Ingress, serviceName string) bool { + spec := ingress.Spec + if ingressBackendMatchesServiceName(spec.DefaultBackend, serviceName) { + return true + } + + for _, rule := range spec.Rules { + if rule.IngressRuleValue.HTTP == nil { + continue + } + for _, path := range rule.IngressRuleValue.HTTP.Paths { + if ingressBackendMatchesServiceName(&path.Backend, serviceName) { + return true + } + } + } + return false +} + +func ingressBackendMatchesServiceName(ingressBackend *networkingv1.IngressBackend, serviceName string) bool { + if ingressBackend == nil { + return false + } + + if ingressBackend.Service != nil && ingressBackend.Service.Name == serviceName { + return true + } + + if ingressBackend.Resource != nil && ingressBackend.Resource.Kind == types.ResourceKindService && ingressBackend.Resource.Name == serviceName { + return true + } + return false +} diff --git a/pkg/resource/ingress/list.go b/pkg/resource/ingress/list.go new file mode 100644 index 0000000..03b614c --- /dev/null +++ b/pkg/resource/ingress/list.go @@ -0,0 +1,104 @@ +package ingress + +import ( + "context" + "github.com/karmada-io/dashboard/pkg/common/errors" + "github.com/karmada-io/dashboard/pkg/common/helpers" + "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/networking/v1" + client "k8s.io/client-go/kubernetes" +) + +// Ingress - a single ingress returned to the frontend. +type Ingress struct { + types.ObjectMeta `json:"objectMeta"` + types.TypeMeta `json:"typeMeta"` + + // External endpoints of this ingress. + Endpoints []common.Endpoint `json:"endpoints"` + Hosts []string `json:"hosts"` +} + +// IngressList - response structure for a queried ingress list. +type IngressList struct { + types.ListMeta `json:"listMeta"` + + // Unordered list of Ingresss. + Items []Ingress `json:"items"` + + // List of non-critical errors, that occurred during resource retrieval. + Errors []error `json:"errors"` +} + +// GetIngressList returns all ingresses in the given namespace. +func GetIngressList(client client.Interface, namespace *common.NamespaceQuery, + dsQuery *dataselect.DataSelectQuery) (*IngressList, error) { + ingressList, err := client.NetworkingV1().Ingresses(namespace.ToRequestParam()).List(context.TODO(), helpers.ListEverything) + + nonCriticalErrors, criticalError := errors.ExtractErrors(err) + if criticalError != nil { + return nil, criticalError + } + + return ToIngressList(ingressList.Items, nonCriticalErrors, dsQuery), nil +} + +func getEndpoints(ingress *v1.Ingress) []common.Endpoint { + endpoints := make([]common.Endpoint, 0) + if len(ingress.Status.LoadBalancer.Ingress) > 0 { + for _, status := range ingress.Status.LoadBalancer.Ingress { + endpoint := common.Endpoint{} + if status.Hostname != "" { + endpoint.Host = status.Hostname + } else if status.IP != "" { + endpoint.Host = status.IP + } + endpoints = append(endpoints, endpoint) + } + } + return endpoints +} + +func getHosts(ingress *v1.Ingress) []string { + hosts := make([]string, 0) + set := make(map[string]struct{}) + + for _, rule := range ingress.Spec.Rules { + if _, exists := set[rule.Host]; !exists && len(rule.Host) > 0 { + hosts = append(hosts, rule.Host) + } + + set[rule.Host] = struct{}{} + } + + return hosts +} + +func toIngress(ingress *v1.Ingress) Ingress { + return Ingress{ + ObjectMeta: types.NewObjectMeta(ingress.ObjectMeta), + TypeMeta: types.NewTypeMeta(types.ResourceKindIngress), + Endpoints: getEndpoints(ingress), + Hosts: getHosts(ingress), + } +} + +func ToIngressList(ingresses []v1.Ingress, nonCriticalErrors []error, dsQuery *dataselect.DataSelectQuery) *IngressList { + newIngressList := &IngressList{ + ListMeta: types.ListMeta{TotalItems: len(ingresses)}, + Items: make([]Ingress, 0), + Errors: nonCriticalErrors, + } + + ingresCells, filteredTotal := dataselect.GenericDataSelectWithFilter(toCells(ingresses), dsQuery) + ingresses = fromCells(ingresCells) + newIngressList.ListMeta = types.ListMeta{TotalItems: filteredTotal} + + for _, ingress := range ingresses { + newIngressList.Items = append(newIngressList.Items, toIngress(&ingress)) + } + + return newIngressList +}