package middleware
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"strings"
"github.com/go-chi/chi"
"github.com/goodrain/rainbond/api/handler"
"github.com/goodrain/rainbond/api/util"
ctxutil "github.com/goodrain/rainbond/api/util/ctx"
"github.com/goodrain/rainbond/db"
dbmodel "github.com/goodrain/rainbond/db/model"
"github.com/goodrain/rainbond/event"
"github.com/goodrain/rainbond/pkg/component/k8s"
rutil "github.com/goodrain/rainbond/util"
httputil "github.com/goodrain/rainbond/util/http"
"github.com/jinzhu/gorm"
"github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var pool []string
func init() {
pool = []string{
"services_status",
}
}
func InitTenant(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
debugRequestBody(r)
tenantName := chi.URLParam(r, "tenant_name")
if tenantName == "" {
httputil.ReturnError(r, w, 404, "cant find tenant")
return
}
tenant, err := db.GetManager().TenantDao().GetTenantIDByName(tenantName)
if err != nil {
logrus.Errorf("get tenant by tenantName error: %s %v", tenantName, err)
if err.Error() == gorm.ErrRecordNotFound.Error() {
httputil.ReturnError(r, w, 404, "cant find tenant")
return
}
httputil.ReturnError(r, w, 500, "get assign tenant uuid failed")
return
}
ctx := context.WithValue(r.Context(), ctxutil.ContextKey("tenant_name"), tenantName)
ctx = context.WithValue(ctx, ctxutil.ContextKey("tenant_id"), tenant.UUID)
ctx = context.WithValue(ctx, ctxutil.ContextKey("tenant"), tenant)
next.ServeHTTP(w, r.WithContext(ctx))
}
return http.HandlerFunc(fn)
}
func InitService(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
serviceAlias := chi.URLParam(r, "service_alias")
if serviceAlias == "" {
httputil.ReturnError(r, w, 404, "cant find service alias")
return
}
tenantID := r.Context().Value(ctxutil.ContextKey("tenant_id"))
if r.Method == "DELETE" && r.URL.Path == "/v2/tenants/"+chi.URLParam(r, "tenant_name")+"/services/"+serviceAlias+"/" {
if err := cleanupKubernetesResources(tenantID.(string), serviceAlias); err != nil {
logrus.Errorf("cleanup kubernetes resources error: %v", err)
}
}
service, err := db.GetManager().TenantServiceDao().GetServiceByTenantIDAndServiceAlias(tenantID.(string), serviceAlias)
if err != nil {
if err.Error() == gorm.ErrRecordNotFound.Error() {
httputil.ReturnError(r, w, 404, "cant find service")
return
}
logrus.Errorf("get service by tenant & service alias error, %v", err)
httputil.ReturnError(r, w, 500, "get service id error")
return
}
serviceID := service.ServiceID
ctx := context.WithValue(r.Context(), ctxutil.ContextKey("service_alias"), serviceAlias)
ctx = context.WithValue(ctx, ctxutil.ContextKey("service_id"), serviceID)
ctx = context.WithValue(ctx, ctxutil.ContextKey("service"), service)
next.ServeHTTP(w, r.WithContext(ctx))
}
return http.HandlerFunc(fn)
}
func cleanupKubernetesResources(tenantID, serviceAlias string) error {
tenant, err := db.GetManager().TenantDao().GetTenantByUUID(tenantID)
if err != nil {
logrus.Errorf("get tenant by id error: %v", err)
return err
}
namespace := tenant.Namespace
if namespace == "" {
namespace = tenantID
}
if err := cleanupApisixRoutes(namespace, serviceAlias); err != nil {
logrus.Errorf("cleanup apisix routes error: %v", err)
}
if err := cleanupServices(namespace, serviceAlias); err != nil {
logrus.Errorf("cleanup services error: %v", err)
}
logrus.Infof("cleanup kubernetes resources for service %s in namespace %s completed", serviceAlias, namespace)
return nil
}
func cleanupApisixRoutes(namespace, serviceAlias string) error {
ctx := context.Background()
err := k8s.Default().ApiSixClient.ApisixV2().ApisixRoutes(namespace).DeleteCollection(
ctx,
metav1.DeleteOptions{},
metav1.ListOptions{
LabelSelector: "component_sort=" + serviceAlias,
},
)
if err != nil {
logrus.Errorf("delete apisix routes for component %s error: %v", serviceAlias, err)
return err
}
logrus.Infof("deleted apisix routes for component %s in namespace %s", serviceAlias, namespace)
return nil
}
func cleanupServices(namespace, serviceAlias string) error {
ctx := context.Background()
serviceList, err := k8s.Default().Clientset.CoreV1().Services(namespace).List(
ctx,
metav1.ListOptions{
LabelSelector: "service_alias=" + serviceAlias,
},
)
if err != nil {
logrus.Errorf("list services for component %s error: %v", serviceAlias, err)
return err
}
for _, svc := range serviceList.Items {
if err := k8s.Default().Clientset.CoreV1().Services(namespace).Delete(
ctx,
svc.Name,
metav1.DeleteOptions{},
); err != nil {
logrus.Warningf("delete service(%s): %v", svc.GetName(), err)
} else {
logrus.Infof("deleted service: %s", svc.GetName())
}
}
logrus.Infof("cleanup services for component %s in namespace %s completed", serviceAlias, namespace)
return nil
}
func InitApplication(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
appID := chi.URLParam(r, "app_id")
tenantApp, err := handler.GetApplicationHandler().GetAppByID(appID)
if err != nil {
httputil.ReturnBcodeError(r, w, err)
return
}
ctx := context.WithValue(r.Context(), ctxutil.ContextKey("app_id"), tenantApp.AppID)
ctx = context.WithValue(ctx, ctxutil.ContextKey("application"), tenantApp)
next.ServeHTTP(w, r.WithContext(ctx))
}
return http.HandlerFunc(fn)
}
func InitPlugin(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
debugRequestBody(r)
pluginID := chi.URLParam(r, "plugin_id")
tenantID := r.Context().Value(ctxutil.ContextKey("tenant_id")).(string)
if pluginID == "" {
httputil.ReturnError(r, w, 404, "need plugin id")
return
}
_, err := db.GetManager().TenantPluginDao().GetPluginByID(pluginID, tenantID)
if err != nil {
if err.Error() == gorm.ErrRecordNotFound.Error() {
httputil.ReturnError(r, w, 404, "cant find plugin")
return
}
logrus.Errorf("get plugin error, %v", err)
httputil.ReturnError(r, w, 500, "get plugin error")
return
}
ctx := context.WithValue(r.Context(), ctxutil.ContextKey("plugin_id"), pluginID)
next.ServeHTTP(w, r.WithContext(ctx))
}
return http.HandlerFunc(fn)
}
func SetLog(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
eventID := chi.URLParam(r, "event_id")
if eventID != "" {
logger := event.GetManager().GetLogger(eventID)
ctx := context.WithValue(r.Context(), ctxutil.ContextKey("logger"), logger)
next.ServeHTTP(w, r.WithContext(ctx))
}
}
return http.HandlerFunc(fn)
}
func Proxy(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.RequestURI, "/v2/nodes") {
handler.GetNodeProxy().Proxy(w, r)
return
}
if strings.HasPrefix(r.RequestURI, "/v2/cluster/service-health") {
handler.GetNodeProxy().Proxy(w, r)
return
}
if strings.HasPrefix(r.RequestURI, "/v2/builder") {
handler.GetBuilderProxy().Proxy(w, r)
return
}
if strings.HasPrefix(r.RequestURI, "/v2/tasks") {
handler.GetNodeProxy().Proxy(w, r)
return
}
if strings.HasPrefix(r.RequestURI, "/v2/tasktemps") {
handler.GetNodeProxy().Proxy(w, r)
return
}
if strings.HasPrefix(r.RequestURI, "/v2/taskgroups") {
handler.GetNodeProxy().Proxy(w, r)
return
}
if strings.HasPrefix(r.RequestURI, "/v2/configs") {
handler.GetNodeProxy().Proxy(w, r)
return
}
if strings.HasPrefix(r.RequestURI, "/kubernetes/dashboard") {
proxy := handler.GetKubernetesDashboardProxy()
r.URL.Path = strings.Replace(r.URL.Path, "/kubernetes/dashboard", "", 1)
proxy.Proxy(w, r)
return
}
if strings.HasPrefix(r.RequestURI, "/v2/container_disk") {
handler.GetNodeProxy().Proxy(w, r)
return
}
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
func apiExclude(r *http.Request) bool {
if r.Method == "GET" {
return true
}
for _, item := range pool {
if strings.Contains(r.RequestURI, item) {
return true
}
}
return false
}
type resWriter struct {
origWriter http.ResponseWriter
statusCode int
}
func (w *resWriter) Header() http.Header {
return w.origWriter.Header()
}
func (w *resWriter) Write(p []byte) (int, error) {
return w.origWriter.Write(p)
}
func (w *resWriter) WriteHeader(statusCode int) {
w.statusCode = statusCode
w.origWriter.WriteHeader(statusCode)
}
func WrapEL(f http.HandlerFunc, target, optType string, synType int, resourceValidation bool) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := LicenseVerification(r, resourceValidation)
if err != nil {
err.Handle(r, w)
return
}
var (
serviceKind string
)
serviceObj := r.Context().Value(ctxutil.ContextKey("service"))
if serviceObj != nil {
service := serviceObj.(*dbmodel.TenantServices)
serviceKind = service.Kind
}
if r.Method != "GET" {
body, err := io.ReadAll(r.Body)
if err != nil {
logrus.Warningf("error reading request body: %v", err)
} else {
logrus.Debugf("method: %s; uri: %s; body: %s", r.Method, r.RequestURI, string(body))
}
r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
var targetID string
var ok bool
if targetID, ok = r.Context().Value(ctxutil.ContextKey("service_id")).(string); !ok {
var reqDataMap map[string]interface{}
if err = json.Unmarshal(body, &reqDataMap); err != nil {
httputil.ReturnError(r, w, 400, "操作对象未指定")
return
}
if targetID, ok = reqDataMap["service_id"].(string); !ok {
httputil.ReturnError(r, w, 400, "操作对象未指定")
return
}
}
if !util.CanDoEvent(optType, synType, target, targetID, serviceKind) {
logrus.Errorf("operation too frequently. uri: %s; target: %s; target id: %s", r.RequestURI, target, targetID)
httputil.ReturnError(r, w, 409, "操作过于频繁,请稍后再试")
return
}
var operator string
var reqData map[string]interface{}
if err = json.Unmarshal(body, &reqData); err == nil {
if operatorI := reqData["operator"]; operatorI != nil {
operator = operatorI.(string)
}
}
tenantID := r.Context().Value(ctxutil.ContextKey("tenant_id")).(string)
var ctx context.Context
skipEventCreation := false
buildKind := ""
if kindI, ok := reqData["kind"]; ok {
buildKind, _ = kindI.(string)
}
if optType == "build-service" && buildKind == "build_from_source_code" && shouldDeferBuildEvent() {
logrus.Infof("Source-scan plugin detected for source code build, deferring build-service event creation until after scan")
skipEventCreation = true
}
if skipEventCreation {
eventID := rutil.NewUUID()
ctx = context.WithValue(r.Context(), ctxutil.ContextKey("event_id"), eventID)
ctx = context.WithValue(ctx, ctxutil.ContextKey("deferred_event"), true)
ctx = context.WithValue(ctx, ctxutil.ContextKey("deferred_event_params"), map[string]interface{}{
"target": target,
"optType": optType,
"targetID": targetID,
"tenantID": tenantID,
"body": string(body),
"operator": operator,
"synType": synType,
})
rw := &resWriter{origWriter: w}
f(rw, r.WithContext(ctx))
} else {
event, err := util.CreateEvent(target, optType, targetID, tenantID, string(body), operator, "", "", synType)
if err != nil {
logrus.Error("create event error : ", err)
httputil.ReturnError(r, w, 500, "操作失败")
return
}
ctx = context.WithValue(r.Context(), ctxutil.ContextKey("event"), event)
ctx = context.WithValue(ctx, ctxutil.ContextKey("event_id"), event.EventID)
rw := &resWriter{origWriter: w}
f(rw, r.WithContext(ctx))
if synType == dbmodel.SYNEVENTTYPE || (synType == dbmodel.ASYNEVENTTYPE && rw.statusCode >= 400) {
util.UpdateEvent(event.EventID, rw.statusCode)
}
}
}
}
}
func shouldDeferBuildEvent() bool {
ctx := context.Background()
plugin, err := k8s.Default().RainbondClient.RainbondV1alpha1().RBDPlugins(metav1.NamespaceAll).Get(ctx, "rainbond-sourcescan", metav1.GetOptions{})
if err != nil {
logrus.Debugf("Source-scan plugin not found: %v", err)
return false
}
if plugin.Spec.BackendService == "" {
logrus.Debug("Source-scan plugin found but BackendService is empty")
return false
}
logrus.Infof("Source-scan plugin active with backend: %s, deferring build event creation", plugin.Spec.BackendService)
return true
}
func debugRequestBody(r *http.Request) {
if !apiExclude(r) {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
logrus.Warningf("error reading request body: %v", err)
}
logrus.Debugf("method: %s; uri: %s; body: %s", r.Method, r.RequestURI, string(body))
r.Body = io.NopCloser(bytes.NewBuffer(body))
}
}
func LicenseVerification(r *http.Request, resourceValidation bool) *util.APIHandleError {
if !resourceValidation {
return nil
}
_, err := k8s.Default().RainbondClient.RainbondV1alpha1().RBDPlugins(metav1.NamespaceNone).Get(context.TODO(), "rainbond-enterprise-base", metav1.GetOptions{})
if err != nil {
return nil
}
if verifyRSALicense() {
return nil
}
return util.CreateAPIHandleError(412, fmt.Errorf("authorize_cluster_lack_of_license"))
}