package apigateway
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/go-chi/chi"
"github.com/goodrain/rainbond/api/model"
"github.com/goodrain/rainbond/api/util/bcode"
ctxutil "github.com/goodrain/rainbond/api/util/ctx"
dbmodel "github.com/goodrain/rainbond/db/model"
"github.com/goodrain/rainbond/pkg/component/k8s"
httputil "github.com/goodrain/rainbond/util/http"
"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/intstr"
)
func generateAccessURLs(ips []string, ports []model.LoadBalancerPort, useServicePort bool) []string {
var accessURLs []string
for _, ip := range ips {
for _, port := range ports {
var portNum int
if useServicePort {
portNum = port.Port
} else {
if port.NodePort > 0 {
portNum = int(port.NodePort)
} else {
continue
}
}
url := fmt.Sprintf("%s:%d", ip, portNum)
accessURLs = append(accessURLs, url)
}
}
return accessURLs
}
func (g Struct) CreateLoadBalancer(w http.ResponseWriter, r *http.Request) {
tenant := r.Context().Value(ctxutil.ContextKey("tenant")).(*dbmodel.Tenants)
serviceID := r.URL.Query().Get("service_id")
appID := r.URL.Query().Get("appID")
k := k8s.Default().Clientset.CoreV1()
var createLBReq model.CreateLoadBalancerStruct
if !httputil.ValidatorRequestStructAndErrorResponse(r, w, &createLBReq, nil) {
return
}
if len(createLBReq.Ports) == 0 {
httputil.ReturnBcodeError(r, w, bcode.NewBadRequest("ports cannot be empty"))
return
}
for _, port := range createLBReq.Ports {
protocol := strings.ToUpper(port.Protocol)
if protocol != "TCP" && protocol != "UDP" {
httputil.ReturnBcodeError(r, w, bcode.NewBadRequest("protocol must be TCP or UDP"))
return
}
}
serviceName := fmt.Sprintf("%s-lb", createLBReq.ServiceName)
_, err := k.Services(tenant.Namespace).Get(r.Context(), serviceName, v1.GetOptions{})
if err == nil {
httputil.ReturnBcodeError(r, w, bcode.NewBadRequest("LoadBalancer service already exists"))
return
}
if !errors.IsNotFound(err) {
logrus.Errorf("check LoadBalancer service error %s", err.Error())
httputil.ReturnBcodeError(r, w, bcode.ServerErr)
return
}
var servicePorts []corev1.ServicePort
for _, port := range createLBReq.Ports {
portName := port.Name
if portName == "" {
portName = fmt.Sprintf("port-%d", port.Port)
}
servicePorts = append(servicePorts, corev1.ServicePort{
Name: portName,
Protocol: corev1.Protocol(strings.ToUpper(port.Protocol)),
Port: int32(port.Port),
TargetPort: intstr.FromInt(port.TargetPort),
})
}
spec := corev1.ServiceSpec{
Type: corev1.ServiceTypeLoadBalancer,
Ports: servicePorts,
}
spec.Selector = map[string]string{
"service_alias": createLBReq.ServiceName,
}
labels := make(map[string]string)
labels["creator"] = "Rainbond"
labels["loadbalancer"] = "true"
if appID != "" {
labels["app_id"] = appID
}
if serviceID != "" {
labels["service_id"] = serviceID
}
labels["service_alias"] = createLBReq.ServiceName
var portStrings []string
for _, port := range createLBReq.Ports {
portStrings = append(portStrings, fmt.Sprintf("%d", port.Port))
}
labels["ports"] = strings.Join(portStrings, "_")
annotations := make(map[string]string)
if createLBReq.Annotations != nil {
for k, v := range createLBReq.Annotations {
annotations[k] = v
}
}
service := &corev1.Service{
ObjectMeta: v1.ObjectMeta{
Name: serviceName,
Labels: labels,
Annotations: annotations,
},
Spec: spec,
}
createdService, err := k.Services(tenant.Namespace).Create(r.Context(), service, v1.CreateOptions{})
if err != nil {
logrus.Errorf("create LoadBalancer service error %s", err.Error())
httputil.ReturnBcodeError(r, w, fmt.Errorf("create LoadBalancer service error: %s", err.Error()))
return
}
var responsePorts []model.LoadBalancerPort
for _, port := range createdService.Spec.Ports {
responsePorts = append(responsePorts, model.LoadBalancerPort{
Port: int(port.Port),
TargetPort: port.TargetPort.IntValue(),
Protocol: string(port.Protocol),
Name: port.Name,
NodePort: port.NodePort,
})
}
var accessURLs []string
var ingressIPs []string
if len(createdService.Status.LoadBalancer.Ingress) > 0 {
for _, ingress := range createdService.Status.LoadBalancer.Ingress {
if ingress.IP != "" {
ingressIPs = append(ingressIPs, ingress.IP)
}
if ingress.Hostname != "" {
ingressIPs = append(ingressIPs, ingress.Hostname)
}
}
}
if len(ingressIPs) > 0 {
accessURLs = generateAccessURLs(ingressIPs, responsePorts, true)
}
response := &model.LoadBalancerResponse{
Name: createdService.Name,
Namespace: createdService.Namespace,
ServiceName: createLBReq.ServiceName,
Ports: responsePorts,
ExternalIPs: createdService.Spec.ExternalIPs,
AccessURLs: accessURLs,
Annotations: createdService.Annotations,
Status: "Creating",
CreatedAt: createdService.CreationTimestamp.Format(time.RFC3339),
}
if len(createdService.Status.LoadBalancer.Ingress) > 0 {
response.Status = "Ready"
var externalIPs []string
for _, ingress := range createdService.Status.LoadBalancer.Ingress {
if ingress.IP != "" {
externalIPs = append(externalIPs, ingress.IP)
}
if ingress.Hostname != "" {
externalIPs = append(externalIPs, ingress.Hostname)
}
}
response.ExternalIPs = externalIPs
}
logrus.Infof("LoadBalancer service created successfully: %s", serviceName)
httputil.ReturnSuccess(r, w, response)
}
func (g Struct) GetLoadBalancer(w http.ResponseWriter, r *http.Request) {
tenant := r.Context().Value(ctxutil.ContextKey("tenant")).(*dbmodel.Tenants)
k := k8s.Default().Clientset.CoreV1()
appID := r.URL.Query().Get("intID")
serviceName := r.URL.Query().Get("service_name")
labelSelector := "loadbalancer=true"
if appID != "" {
labelSelector += ",app_id=" + appID
}
if serviceName != "" {
labelSelector += ",service_alias=" + serviceName
}
list, err := k.Services(tenant.Namespace).List(r.Context(), v1.ListOptions{
LabelSelector: labelSelector,
})
if err != nil {
logrus.Errorf("get LoadBalancer services error %s", err.Error())
httputil.ReturnBcodeError(r, w, bcode.ServerErr)
return
}
var responses []*model.LoadBalancerResponse
for _, service := range list.Items {
if service.Spec.Type != corev1.ServiceTypeLoadBalancer {
continue
}
var servicePorts []model.LoadBalancerPort
for _, port := range service.Spec.Ports {
servicePorts = append(servicePorts, model.LoadBalancerPort{
Port: int(port.Port),
TargetPort: port.TargetPort.IntValue(),
Protocol: string(port.Protocol),
Name: port.Name,
NodePort: port.NodePort,
})
}
var accessURLs []string
var ingressIPs []string
if len(service.Status.LoadBalancer.Ingress) > 0 {
for _, ingress := range service.Status.LoadBalancer.Ingress {
if ingress.IP != "" {
ingressIPs = append(ingressIPs, ingress.IP)
}
if ingress.Hostname != "" {
ingressIPs = append(ingressIPs, ingress.Hostname)
}
}
}
if len(ingressIPs) > 0 {
accessURLs = generateAccessURLs(ingressIPs, servicePorts, true)
}
response := &model.LoadBalancerResponse{
Name: service.Name,
Namespace: service.Namespace,
ServiceName: service.Labels["service_alias"],
Ports: servicePorts,
ExternalIPs: service.Spec.ExternalIPs,
AccessURLs: accessURLs,
Annotations: service.Annotations,
Status: "Creating",
CreatedAt: service.CreationTimestamp.Format(time.RFC3339),
}
if len(service.Status.LoadBalancer.Ingress) > 0 {
response.Status = "Ready"
var externalIPs []string
for _, ingress := range service.Status.LoadBalancer.Ingress {
if ingress.IP != "" {
externalIPs = append(externalIPs, ingress.IP)
}
if ingress.Hostname != "" {
externalIPs = append(externalIPs, ingress.Hostname)
}
}
if len(externalIPs) > 0 {
response.ExternalIPs = externalIPs
}
}
responses = append(responses, response)
}
httputil.ReturnSuccess(r, w, responses)
}
func (g Struct) DeleteLoadBalancer(w http.ResponseWriter, r *http.Request) {
tenant := r.Context().Value(ctxutil.ContextKey("tenant")).(*dbmodel.Tenants)
serviceName := chi.URLParam(r, "name")
if serviceName == "" {
serviceName = r.URL.Query().Get("name")
}
if serviceName == "" {
httputil.ReturnBcodeError(r, w, bcode.NewBadRequest("service name is required"))
return
}
k := k8s.Default().Clientset.CoreV1()
service, err := k.Services(tenant.Namespace).Get(r.Context(), serviceName, v1.GetOptions{})
if err != nil {
if errors.IsNotFound(err) {
httputil.ReturnBcodeError(r, w, bcode.NotFound)
return
}
logrus.Errorf("get LoadBalancer service error %s", err.Error())
httputil.ReturnBcodeError(r, w, bcode.ServerErr)
return
}
if service.Spec.Type != corev1.ServiceTypeLoadBalancer {
httputil.ReturnBcodeError(r, w, bcode.NewBadRequest("service is not a LoadBalancer type"))
return
}
if service.Labels["creator"] != "Rainbond" || service.Labels["loadbalancer"] != "true" {
httputil.ReturnBcodeError(r, w, bcode.NewBadRequest("service is not a Rainbond LoadBalancer"))
return
}
err = k.Services(tenant.Namespace).Delete(r.Context(), serviceName, v1.DeleteOptions{})
if err != nil && !errors.IsNotFound(err) {
logrus.Errorf("delete LoadBalancer service error %s", err.Error())
httputil.ReturnBcodeError(r, w, bcode.ServerErr)
return
}
logrus.Infof("LoadBalancer service deleted successfully: %s", serviceName)
httputil.ReturnSuccess(r, w, map[string]string{
"message": "LoadBalancer service deleted successfully",
"name": serviceName,
})
}
func (g Struct) UpdateLoadBalancer(w http.ResponseWriter, r *http.Request) {
tenant := r.Context().Value(ctxutil.ContextKey("tenant")).(*dbmodel.Tenants)
serviceName := chi.URLParam(r, "name")
if serviceName == "" {
serviceName = r.URL.Query().Get("name")
}
if serviceName == "" {
httputil.ReturnBcodeError(r, w, bcode.NewBadRequest("service name is required"))
return
}
var updateLBReq model.UpdateLoadBalancerStruct
if !httputil.ValidatorRequestStructAndErrorResponse(r, w, &updateLBReq, nil) {
return
}
k := k8s.Default().Clientset.CoreV1()
service, err := k.Services(tenant.Namespace).Get(r.Context(), serviceName, v1.GetOptions{})
if err != nil {
if errors.IsNotFound(err) {
httputil.ReturnBcodeError(r, w, bcode.NotFound)
return
}
logrus.Errorf("get LoadBalancer service error %s", err.Error())
httputil.ReturnBcodeError(r, w, bcode.ServerErr)
return
}
if service.Spec.Type != corev1.ServiceTypeLoadBalancer {
httputil.ReturnBcodeError(r, w, bcode.NewBadRequest("service is not a LoadBalancer type"))
return
}
if service.Labels["creator"] != "Rainbond" || service.Labels["loadbalancer"] != "true" {
httputil.ReturnBcodeError(r, w, bcode.NewBadRequest("service is not a Rainbond LoadBalancer"))
return
}
if len(updateLBReq.Ports) > 0 {
for _, port := range updateLBReq.Ports {
protocol := strings.ToUpper(port.Protocol)
if protocol != "TCP" && protocol != "UDP" {
httputil.ReturnBcodeError(r, w, bcode.NewBadRequest("protocol must be TCP or UDP"))
return
}
}
var servicePorts []corev1.ServicePort
for _, port := range updateLBReq.Ports {
portName := port.Name
if portName == "" {
portName = fmt.Sprintf("port-%d", port.Port)
}
servicePorts = append(servicePorts, corev1.ServicePort{
Name: portName,
Protocol: corev1.Protocol(strings.ToUpper(port.Protocol)),
Port: int32(port.Port),
TargetPort: intstr.FromInt(port.TargetPort),
})
}
service.Spec.Ports = servicePorts
var updatePortStrings []string
for _, port := range updateLBReq.Ports {
updatePortStrings = append(updatePortStrings, fmt.Sprintf("%d", port.Port))
}
service.Labels["ports"] = strings.Join(updatePortStrings, "_")
}
if updateLBReq.Annotations != nil {
if service.Annotations == nil {
service.Annotations = make(map[string]string)
}
for k, v := range updateLBReq.Annotations {
service.Annotations[k] = v
}
}
updatedService, err := k.Services(tenant.Namespace).Update(r.Context(), service, v1.UpdateOptions{})
if err != nil {
logrus.Errorf("update LoadBalancer service error %s", err.Error())
httputil.ReturnBcodeError(r, w, fmt.Errorf("update LoadBalancer service error: %s", err.Error()))
return
}
var updatedPorts []model.LoadBalancerPort
for _, port := range updatedService.Spec.Ports {
updatedPorts = append(updatedPorts, model.LoadBalancerPort{
Port: int(port.Port),
TargetPort: port.TargetPort.IntValue(),
Protocol: string(port.Protocol),
Name: port.Name,
NodePort: port.NodePort,
})
}
var accessURLs []string
var ingressIPs []string
if len(updatedService.Status.LoadBalancer.Ingress) > 0 {
for _, ingress := range updatedService.Status.LoadBalancer.Ingress {
if ingress.IP != "" {
ingressIPs = append(ingressIPs, ingress.IP)
}
if ingress.Hostname != "" {
ingressIPs = append(ingressIPs, ingress.Hostname)
}
}
}
if len(ingressIPs) > 0 {
accessURLs = generateAccessURLs(ingressIPs, updatedPorts, true)
}
response := &model.LoadBalancerResponse{
Name: updatedService.Name,
Namespace: updatedService.Namespace,
ServiceName: updatedService.Labels["service_alias"],
Ports: updatedPorts,
ExternalIPs: updatedService.Spec.ExternalIPs,
AccessURLs: accessURLs,
Annotations: updatedService.Annotations,
Status: "Creating",
CreatedAt: updatedService.CreationTimestamp.Format(time.RFC3339),
}
if len(updatedService.Status.LoadBalancer.Ingress) > 0 {
response.Status = "Ready"
var externalIPs []string
for _, ingress := range updatedService.Status.LoadBalancer.Ingress {
if ingress.IP != "" {
externalIPs = append(externalIPs, ingress.IP)
}
if ingress.Hostname != "" {
externalIPs = append(externalIPs, ingress.Hostname)
}
}
if len(externalIPs) > 0 {
response.ExternalIPs = externalIPs
}
}
logrus.Infof("LoadBalancer service updated successfully: %s", serviceName)
httputil.ReturnSuccess(r, w, response)
}