Skip to content

Commit

Permalink
Merge pull request #114 from warjiang/feature/service-management
Browse files Browse the repository at this point in the history
feature: add service management
  • Loading branch information
karmada-bot authored Sep 10, 2024
2 parents 705986f + 166891c commit ba0fef4
Show file tree
Hide file tree
Showing 11 changed files with 564 additions and 0 deletions.
1 change: 1 addition & 0 deletions cmd/api/app/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down
54 changes: 54 additions & 0 deletions cmd/api/app/routes/service/handler.go
Original file line number Diff line number Diff line change
@@ -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)
}
81 changes: 81 additions & 0 deletions pkg/resource/common/endpoint.go
Original file line number Diff line number Diff line change
@@ -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),
}
}
20 changes: 20 additions & 0 deletions pkg/resource/common/resourcechannels.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
38 changes: 38 additions & 0 deletions pkg/resource/common/serviceport.go
Original file line number Diff line number Diff line change
@@ -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
}
97 changes: 97 additions & 0 deletions pkg/resource/endpoint/endpoint.go
Original file line number Diff line number Diff line change
@@ -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,
}
}
47 changes: 47 additions & 0 deletions pkg/resource/endpoint/list.go
Original file line number Diff line number Diff line change
@@ -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
}
42 changes: 42 additions & 0 deletions pkg/resource/service/common.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit ba0fef4

Please sign in to comment.