Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add api for ingress resource #141

Merged
merged 1 commit into from
Oct 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/api/app/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
40 changes: 40 additions & 0 deletions cmd/api/app/routes/ingress/handler.go
Original file line number Diff line number Diff line change
@@ -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)
}
40 changes: 40 additions & 0 deletions pkg/resource/ingress/common.go
Original file line number Diff line number Diff line change
@@ -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
}
47 changes: 47 additions & 0 deletions pkg/resource/ingress/detail.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
50 changes: 50 additions & 0 deletions pkg/resource/ingress/filter.go
Original file line number Diff line number Diff line change
@@ -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
}
104 changes: 104 additions & 0 deletions pkg/resource/ingress/list.go
Original file line number Diff line number Diff line change
@@ -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
}