From 9bf73407ad71c4450e7ae3bae465a2c69b9466ef Mon Sep 17 00:00:00 2001 From: warjiang <1096409085@qq.com> Date: Mon, 9 Sep 2024 16:31:07 +0800 Subject: [PATCH 1/3] feat: lift code about endpoint from kubernetes dashboard Signed-off-by: warjiang <1096409085@qq.com> --- pkg/resource/common/endpoint.go | 81 +++++++++++++++++++++ pkg/resource/common/resourcechannels.go | 20 +++++ pkg/resource/common/serviceport.go | 38 ++++++++++ pkg/resource/endpoint/endpoint.go | 97 +++++++++++++++++++++++++ pkg/resource/endpoint/list.go | 47 ++++++++++++ 5 files changed, 283 insertions(+) create mode 100644 pkg/resource/common/endpoint.go create mode 100644 pkg/resource/common/serviceport.go create mode 100644 pkg/resource/endpoint/endpoint.go create mode 100644 pkg/resource/endpoint/list.go diff --git a/pkg/resource/common/endpoint.go b/pkg/resource/common/endpoint.go new file mode 100644 index 0000000..df7cd3f --- /dev/null +++ b/pkg/resource/common/endpoint.go @@ -0,0 +1,81 @@ +// Copyright 2017 The Kubernetes 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 common + +import ( + "bytes" + + api "k8s.io/api/core/v1" +) + +// Endpoint describes an endpoint that is host and a list of available ports for that host. +type Endpoint struct { + // Hostname, either as a domain name or IP address. + Host string `json:"host"` + + // List of ports opened for this endpoint on the hostname. + Ports []ServicePort `json:"ports"` +} + +// GetExternalEndpoints returns endpoints that are externally reachable for a service. +func GetExternalEndpoints(service *api.Service) []Endpoint { + externalEndpoints := make([]Endpoint, 0) + if service.Spec.Type == api.ServiceTypeLoadBalancer { + for _, ingress := range service.Status.LoadBalancer.Ingress { + externalEndpoints = append(externalEndpoints, getExternalEndpoint(ingress, service.Spec.Ports)) + } + } + + for _, ip := range service.Spec.ExternalIPs { + externalEndpoints = append(externalEndpoints, Endpoint{ + Host: ip, + Ports: GetServicePorts(service.Spec.Ports), + }) + } + + return externalEndpoints +} + +// GetInternalEndpoint returns internal endpoint name for the given service properties, e.g., +// "my-service.namespace 80/TCP" or "my-service 53/TCP,53/UDP". +func GetInternalEndpoint(serviceName, namespace string, ports []api.ServicePort) Endpoint { + name := serviceName + + if namespace != api.NamespaceDefault && len(namespace) > 0 && len(serviceName) > 0 { + bufferName := bytes.NewBufferString(name) + bufferName.WriteString(".") + bufferName.WriteString(namespace) + name = bufferName.String() + } + + return Endpoint{ + Host: name, + Ports: GetServicePorts(ports), + } +} + +// Returns external endpoint name for the given service properties. +func getExternalEndpoint(ingress api.LoadBalancerIngress, ports []api.ServicePort) Endpoint { + var host string + if ingress.Hostname != "" { + host = ingress.Hostname + } else { + host = ingress.IP + } + return Endpoint{ + Host: host, + Ports: GetServicePorts(ports), + } +} diff --git a/pkg/resource/common/resourcechannels.go b/pkg/resource/common/resourcechannels.go index 1544a46..9096b1c 100644 --- a/pkg/resource/common/resourcechannels.go +++ b/pkg/resource/common/resourcechannels.go @@ -324,6 +324,26 @@ type EndpointListChannel struct { Error chan error } +// GetEndpointListChannelWithOptions is GetEndpointListChannel plus list options. +func GetEndpointListChannelWithOptions(client client.Interface, + nsQuery *NamespaceQuery, opt metaV1.ListOptions, numReads int) EndpointListChannel { + channel := EndpointListChannel{ + List: make(chan *v1.EndpointsList, numReads), + Error: make(chan error, numReads), + } + + go func() { + list, err := client.CoreV1().Endpoints(nsQuery.ToRequestParam()).List(context.TODO(), opt) + + for i := 0; i < numReads; i++ { + channel.List <- list + channel.Error <- err + } + }() + + return channel +} + // IngressListChannel is a list and error channels to Ingresss. type IngressListChannel struct { List chan *networkingv1.IngressList diff --git a/pkg/resource/common/serviceport.go b/pkg/resource/common/serviceport.go new file mode 100644 index 0000000..705f2a1 --- /dev/null +++ b/pkg/resource/common/serviceport.go @@ -0,0 +1,38 @@ +// Copyright 2017 The Kubernetes 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 common + +import api "k8s.io/api/core/v1" + +// ServicePort is a pair of port and protocol, e.g. a service endpoint. +type ServicePort struct { + // Positive port number. + Port int32 `json:"port"` + + // Protocol name, e.g., TCP or UDP. + Protocol api.Protocol `json:"protocol"` + + // The port on each node on which service is exposed. + NodePort int32 `json:"nodePort"` +} + +// GetServicePorts returns human readable name for the given service ports list. +func GetServicePorts(apiPorts []api.ServicePort) []ServicePort { + var ports []ServicePort + for _, port := range apiPorts { + ports = append(ports, ServicePort{port.Port, port.Protocol, port.NodePort}) + } + return ports +} diff --git a/pkg/resource/endpoint/endpoint.go b/pkg/resource/endpoint/endpoint.go new file mode 100644 index 0000000..215809c --- /dev/null +++ b/pkg/resource/endpoint/endpoint.go @@ -0,0 +1,97 @@ +// Copyright 2017 The Kubernetes 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 endpoint + +import ( + "github.com/karmada-io/dashboard/pkg/common/types" + "github.com/karmada-io/dashboard/pkg/resource/common" + "log" + + v1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/fields" + "k8s.io/apimachinery/pkg/labels" + k8sClient "k8s.io/client-go/kubernetes" +) + +type Endpoint struct { + ObjectMeta types.ObjectMeta `json:"objectMeta"` + TypeMeta types.TypeMeta `json:"typeMeta"` + + // Hostname, either as a domain name or IP address. + Host string `json:"host"` + + // Name of the node the endpoint is located + NodeName *string `json:"nodeName"` + + // Status of the endpoint + Ready bool `json:"ready"` + + // Array of endpoint ports + Ports []v1.EndpointPort `json:"ports"` +} + +// GetServiceEndpoints gets list of endpoints targeted by given label selector in given namespace. +func GetServiceEndpoints(client k8sClient.Interface, namespace, name string) (*EndpointList, error) { + endpointList := &EndpointList{ + Endpoints: make([]Endpoint, 0), + ListMeta: types.ListMeta{TotalItems: 0}, + } + + serviceEndpoints, err := GetEndpoints(client, namespace, name) + if err != nil { + return endpointList, err + } + + endpointList = toEndpointList(serviceEndpoints) + log.Printf("Found %d endpoints related to %s service in %s namespace", len(endpointList.Endpoints), name, namespace) + return endpointList, nil +} + +// GetEndpoints gets endpoints associated to resource with given name. +func GetEndpoints(client k8sClient.Interface, namespace, name string) ([]v1.Endpoints, error) { + fieldSelector, err := fields.ParseSelector("metadata.name" + "=" + name) + if err != nil { + return nil, err + } + + channels := &common.ResourceChannels{ + EndpointList: common.GetEndpointListChannelWithOptions(client, + common.NewSameNamespaceQuery(namespace), + metaV1.ListOptions{ + LabelSelector: labels.Everything().String(), + FieldSelector: fieldSelector.String(), + }, + 1), + } + + endpointList := <-channels.EndpointList.List + if err := <-channels.EndpointList.Error; err != nil { + return nil, err + } + + return endpointList.Items, nil +} + +// toEndpoint converts endpoint api Endpoint to Endpoint model object. +func toEndpoint(address v1.EndpointAddress, ports []v1.EndpointPort, ready bool) *Endpoint { + return &Endpoint{ + TypeMeta: types.NewTypeMeta(types.ResourceKindEndpoint), + Host: address.IP, + Ports: ports, + Ready: ready, + NodeName: address.NodeName, + } +} diff --git a/pkg/resource/endpoint/list.go b/pkg/resource/endpoint/list.go new file mode 100644 index 0000000..5203c43 --- /dev/null +++ b/pkg/resource/endpoint/list.go @@ -0,0 +1,47 @@ +// Copyright 2017 The Kubernetes 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 endpoint + +import ( + "github.com/karmada-io/dashboard/pkg/common/types" + v1 "k8s.io/api/core/v1" +) + +type EndpointList struct { + ListMeta types.ListMeta `json:"listMeta"` + // List of endpoints + Endpoints []Endpoint `json:"endpoints"` +} + +// toEndpointList converts array of api events to endpoint List structure +func toEndpointList(endpoints []v1.Endpoints) *EndpointList { + endpointList := EndpointList{ + Endpoints: make([]Endpoint, 0), + ListMeta: types.ListMeta{TotalItems: len(endpoints)}, + } + + for _, endpoint := range endpoints { + for _, subSets := range endpoint.Subsets { + for _, address := range subSets.Addresses { + endpointList.Endpoints = append(endpointList.Endpoints, *toEndpoint(address, subSets.Ports, true)) + } + for _, notReadyAddress := range subSets.NotReadyAddresses { + endpointList.Endpoints = append(endpointList.Endpoints, *toEndpoint(notReadyAddress, subSets.Ports, false)) + } + } + } + + return &endpointList +} From f4cca3d775b440eb9f630c8ddd6c13d00bb5c9eb Mon Sep 17 00:00:00 2001 From: warjiang <1096409085@qq.com> Date: Mon, 9 Sep 2024 16:33:45 +0800 Subject: [PATCH 2/3] feat: add service resources Signed-off-by: warjiang <1096409085@qq.com> --- pkg/resource/service/common.go | 42 ++++++++++++++ pkg/resource/service/detail.go | 53 +++++++++++++++++ pkg/resource/service/events.go | 29 ++++++++++ pkg/resource/service/list.go | 102 +++++++++++++++++++++++++++++++++ 4 files changed, 226 insertions(+) create mode 100644 pkg/resource/service/common.go create mode 100644 pkg/resource/service/detail.go create mode 100644 pkg/resource/service/events.go create mode 100644 pkg/resource/service/list.go diff --git a/pkg/resource/service/common.go b/pkg/resource/service/common.go new file mode 100644 index 0000000..1e25096 --- /dev/null +++ b/pkg/resource/service/common.go @@ -0,0 +1,42 @@ +package service + +import ( + "github.com/karmada-io/dashboard/pkg/dataselect" + v1 "k8s.io/api/core/v1" +) + +// The code below allows to perform complex data section on []api.Service + +type ServiceCell v1.Service + +func (self ServiceCell) 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) + case dataselect.TypeProperty: + return dataselect.StdComparableString(self.Spec.Type) + default: + // if name is not supported then just return a constant dummy value, sort will have no effect. + return nil + } +} + +func toCells(std []v1.Service) []dataselect.DataCell { + cells := make([]dataselect.DataCell, len(std)) + for i := range std { + cells[i] = ServiceCell(std[i]) + } + return cells +} + +func fromCells(cells []dataselect.DataCell) []v1.Service { + std := make([]v1.Service, len(cells)) + for i := range std { + std[i] = v1.Service(cells[i].(ServiceCell)) + } + return std +} diff --git a/pkg/resource/service/detail.go b/pkg/resource/service/detail.go new file mode 100644 index 0000000..d4eed2e --- /dev/null +++ b/pkg/resource/service/detail.go @@ -0,0 +1,53 @@ +package service + +import ( + "context" + "github.com/karmada-io/dashboard/pkg/common/errors" + "github.com/karmada-io/dashboard/pkg/resource/endpoint" + v1 "k8s.io/api/core/v1" + metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sClient "k8s.io/client-go/kubernetes" + "log" +) + +// Service is a representation of a service. +type ServiceDetail struct { + // Extends list item structure. + Service `json:",inline"` + + // List of Endpoint obj. that are endpoints of this Service. + EndpointList endpoint.EndpointList `json:"endpointList"` + + // Show the value of the SessionAffinity of the Service. + SessionAffinity v1.ServiceAffinity `json:"sessionAffinity"` + + // List of non-critical errors, that occurred during resource retrieval. + Errors []error `json:"errors"` +} + +// GetServiceDetail gets service details. +func GetServiceDetail(client k8sClient.Interface, namespace, name string) (*ServiceDetail, error) { + log.Printf("Getting details of %s service in %s namespace", name, namespace) + serviceData, err := client.CoreV1().Services(namespace).Get(context.TODO(), name, metaV1.GetOptions{}) + if err != nil { + return nil, err + } + + endpointList, err := endpoint.GetServiceEndpoints(client, namespace, name) + nonCriticalErrors, criticalError := errors.ExtractErrors(err) + if criticalError != nil { + return nil, criticalError + } + + service := toServiceDetail(serviceData, *endpointList, nonCriticalErrors) + return &service, nil +} + +func toServiceDetail(service *v1.Service, endpointList endpoint.EndpointList, nonCriticalErrors []error) ServiceDetail { + return ServiceDetail{ + Service: toService(service), + EndpointList: endpointList, + SessionAffinity: service.Spec.SessionAffinity, + Errors: nonCriticalErrors, + } +} diff --git a/pkg/resource/service/events.go b/pkg/resource/service/events.go new file mode 100644 index 0000000..34b66cf --- /dev/null +++ b/pkg/resource/service/events.go @@ -0,0 +1,29 @@ +package service + +import ( + "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" + "log" + + client "k8s.io/client-go/kubernetes" +) + +// GetServiceEvents returns model events for a service with the given name in the given namespace. +func GetServiceEvents(client client.Interface, dsQuery *dataselect.DataSelectQuery, namespace, name string) ( + *common.EventList, error) { + eventList := common.EventList{ + Events: make([]common.Event, 0), + ListMeta: types.ListMeta{TotalItems: 0}, + } + + serviceEvents, err := event.GetEvents(client, namespace, name) + if err != nil { + return &eventList, err + } + + eventList = event.CreateEventList(event.FillEventsType(serviceEvents), dsQuery) + log.Printf("Found %d events related to %s service in %s namespace", len(eventList.Events), name, namespace) + return &eventList, nil +} diff --git a/pkg/resource/service/list.go b/pkg/resource/service/list.go new file mode 100644 index 0000000..0ae92d1 --- /dev/null +++ b/pkg/resource/service/list.go @@ -0,0 +1,102 @@ +package service + +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" + v1 "k8s.io/api/core/v1" + client "k8s.io/client-go/kubernetes" + "log" +) + +// Service is a representation of a service. +type Service struct { + ObjectMeta types.ObjectMeta `json:"objectMeta"` + TypeMeta types.TypeMeta `json:"typeMeta"` + + // InternalEndpoint of all Kubernetes services that have the same label selector as connected Replication + // Controller. Endpoint is DNS name merged with ports. + InternalEndpoint common.Endpoint `json:"internalEndpoint"` + + // ExternalEndpoints of all Kubernetes services that have the same label selector as connected Replication + // Controller. Endpoint is external IP address name merged with ports. + ExternalEndpoints []common.Endpoint `json:"externalEndpoints"` + + // Label selector of the service. + Selector map[string]string `json:"selector"` + + // Type determines how the service will be exposed. Valid options: ClusterIP, NodePort, LoadBalancer, ExternalName + Type v1.ServiceType `json:"type"` + + // ClusterIP is usually assigned by the control plane. Valid values are None, empty string (""), or + // a valid IP address. None can be specified for headless services when proxying is not required + ClusterIP string `json:"clusterIP"` +} + +// ServiceList contains a list of services in the cluster. +type ServiceList struct { + ListMeta types.ListMeta `json:"listMeta"` + + // Unordered list of services. + Services []Service `json:"services"` + + // List of non-critical errors, that occurred during resource retrieval. + Errors []error `json:"errors"` +} + +// GetServiceList returns a list of all services in the cluster. +func GetServiceList(client client.Interface, nsQuery *common.NamespaceQuery, + dsQuery *dataselect.DataSelectQuery) (*ServiceList, error) { + log.Print("Getting list of all services in the cluster") + + channels := &common.ResourceChannels{ + ServiceList: common.GetServiceListChannel(client, nsQuery, 1), + } + + return GetServiceListFromChannels(channels, dsQuery) +} + +// GetServiceListFromChannels returns a list of all services in the cluster. +func GetServiceListFromChannels(channels *common.ResourceChannels, + dsQuery *dataselect.DataSelectQuery) (*ServiceList, error) { + services := <-channels.ServiceList.List + err := <-channels.ServiceList.Error + nonCriticalErrors, criticalError := errors.ExtractErrors(err) + if criticalError != nil { + return nil, criticalError + } + + return CreateServiceList(services.Items, nonCriticalErrors, dsQuery), nil +} + +func toService(service *v1.Service) Service { + return Service{ + ObjectMeta: types.NewObjectMeta(service.ObjectMeta), + TypeMeta: types.NewTypeMeta(types.ResourceKindService), + InternalEndpoint: common.GetInternalEndpoint(service.Name, service.Namespace, service.Spec.Ports), + ExternalEndpoints: common.GetExternalEndpoints(service), + Selector: service.Spec.Selector, + ClusterIP: service.Spec.ClusterIP, + Type: service.Spec.Type, + } +} + +// CreateServiceList returns paginated service list based on given service array and pagination query. +func CreateServiceList(services []v1.Service, nonCriticalErrors []error, dsQuery *dataselect.DataSelectQuery) *ServiceList { + serviceList := &ServiceList{ + Services: make([]Service, 0), + ListMeta: types.ListMeta{TotalItems: len(services)}, + Errors: nonCriticalErrors, + } + + serviceCells, filteredTotal := dataselect.GenericDataSelectWithFilter(toCells(services), dsQuery) + services = fromCells(serviceCells) + serviceList.ListMeta = types.ListMeta{TotalItems: filteredTotal} + + for _, service := range services { + serviceList.Services = append(serviceList.Services, toService(&service)) + } + + return serviceList +} From 166891cfd453a081942c466c59dbd45ef142d218 Mon Sep 17 00:00:00 2001 From: warjiang <1096409085@qq.com> Date: Mon, 9 Sep 2024 16:34:33 +0800 Subject: [PATCH 3/3] feat: add api for service management Signed-off-by: warjiang <1096409085@qq.com> --- cmd/api/app/api.go | 1 + cmd/api/app/routes/service/handler.go | 54 +++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 cmd/api/app/routes/service/handler.go diff --git a/cmd/api/app/api.go b/cmd/api/app/api.go index 3ea3df9..1213b60 100644 --- a/cmd/api/app/api.go +++ b/cmd/api/app/api.go @@ -27,6 +27,7 @@ import ( _ "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/secret" + _ "github.com/karmada-io/dashboard/cmd/api/app/routes/service" _ "github.com/karmada-io/dashboard/cmd/api/app/routes/statefulset" _ "github.com/karmada-io/dashboard/cmd/api/app/routes/unstructured" ) diff --git a/cmd/api/app/routes/service/handler.go b/cmd/api/app/routes/service/handler.go new file mode 100644 index 0000000..1fe9b84 --- /dev/null +++ b/cmd/api/app/routes/service/handler.go @@ -0,0 +1,54 @@ +package service + +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/service" +) + +func handleGetServices(c *gin.Context) { + k8sClient := client.InClusterClientForKarmadaApiServer() + dataSelect := common.ParseDataSelectPathParameter(c) + nsQuery := common.ParseNamespacePathParameter(c) + result, err := service.GetServiceList(k8sClient, nsQuery, dataSelect) + if err != nil { + common.Fail(c, err) + return + } + common.Success(c, result) +} + +func handleGetServiceDetail(c *gin.Context) { + k8sClient := client.InClusterClientForKarmadaApiServer() + namespace := c.Param("namespace") + name := c.Param("service") + result, err := service.GetServiceDetail(k8sClient, namespace, name) + if err != nil { + common.Fail(c, err) + return + } + common.Success(c, result) +} + +func handleGetServiceEvents(c *gin.Context) { + k8sClient := client.InClusterClientForKarmadaApiServer() + namespace := c.Param("namespace") + name := c.Param("service") + dataSelect := common.ParseDataSelectPathParameter(c) + result, err := service.GetServiceEvents(k8sClient, dataSelect, namespace, name) + if err != nil { + common.Fail(c, err) + return + } + common.Success(c, result) +} + +func init() { + r := router.V1() + r.GET("/service", handleGetServices) + r.GET("/service/:namespace", handleGetServices) + r.GET("/service/:namespace/:service", handleGetServiceDetail) + r.GET("/service/:namespace/:service/event", handleGetServiceEvents) +}