| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 route |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "net/http" |
| "os" |
| "path/filepath" |
| "reflect" |
| "strconv" |
| "strings" |
| |
| "github.com/gin-gonic/gin" |
| "github.com/shiningrush/droplet" |
| "github.com/shiningrush/droplet/data" |
| "github.com/shiningrush/droplet/wrapper" |
| wgin "github.com/shiningrush/droplet/wrapper/gin" |
| lua "github.com/yuin/gopher-lua" |
| |
| "github.com/apisix/manager-api/internal/conf" |
| "github.com/apisix/manager-api/internal/core/entity" |
| "github.com/apisix/manager-api/internal/core/store" |
| "github.com/apisix/manager-api/internal/handler" |
| "github.com/apisix/manager-api/internal/log" |
| "github.com/apisix/manager-api/internal/utils" |
| "github.com/apisix/manager-api/internal/utils/consts" |
| ) |
| |
| type Handler struct { |
| routeStore store.Interface |
| svcStore store.Interface |
| upstreamStore store.Interface |
| scriptStore store.Interface |
| } |
| |
| func NewHandler() (handler.RouteRegister, error) { |
| return &Handler{ |
| routeStore: store.GetStore(store.HubKeyRoute), |
| svcStore: store.GetStore(store.HubKeyService), |
| upstreamStore: store.GetStore(store.HubKeyUpstream), |
| scriptStore: store.GetStore(store.HubKeyScript), |
| }, nil |
| } |
| |
| func (h *Handler) ApplyRoute(r *gin.Engine) { |
| r.GET("/apisix/admin/routes/:id", wgin.Wraps(h.Get, |
| wrapper.InputType(reflect.TypeOf(GetInput{})))) |
| r.GET("/apisix/admin/routes", wgin.Wraps(h.List, |
| wrapper.InputType(reflect.TypeOf(ListInput{})))) |
| r.POST("/apisix/admin/routes", wgin.Wraps(h.Create, |
| wrapper.InputType(reflect.TypeOf(entity.Route{})))) |
| r.PUT("/apisix/admin/routes", wgin.Wraps(h.Update, |
| wrapper.InputType(reflect.TypeOf(UpdateInput{})))) |
| r.PUT("/apisix/admin/routes/:id", wgin.Wraps(h.Update, |
| wrapper.InputType(reflect.TypeOf(UpdateInput{})))) |
| |
| r.DELETE("/apisix/admin/routes/:ids", wgin.Wraps(h.BatchDelete, |
| wrapper.InputType(reflect.TypeOf(BatchDelete{})))) |
| |
| r.PATCH("/apisix/admin/routes/:id", wgin.Wraps(h.Patch, |
| wrapper.InputType(reflect.TypeOf(PatchInput{})))) |
| r.PATCH("/apisix/admin/routes/:id/*path", wgin.Wraps(h.Patch, |
| wrapper.InputType(reflect.TypeOf(PatchInput{})))) |
| |
| r.GET("/apisix/admin/notexist/routes", wgin.Wraps(h.Exist, |
| wrapper.InputType(reflect.TypeOf(ExistCheckInput{})))) |
| } |
| |
| type PatchInput struct { |
| ID string `auto_read:"id,path"` |
| SubPath string `auto_read:"path,path"` |
| Body []byte `auto_read:"@body"` |
| } |
| |
| func (h *Handler) Patch(c droplet.Context) (interface{}, error) { |
| input := c.Input().(*PatchInput) |
| reqBody := input.Body |
| ID := input.ID |
| subPath := input.SubPath |
| |
| stored, err := h.routeStore.Get(c.Context(), ID) |
| if err != nil { |
| return handler.SpecCodeResponse(err), err |
| } |
| |
| res, err := utils.MergePatch(stored, subPath, reqBody) |
| if err != nil { |
| return handler.SpecCodeResponse(err), err |
| } |
| |
| var route entity.Route |
| err = json.Unmarshal(res, &route) |
| if err != nil { |
| return handler.SpecCodeResponse(err), err |
| } |
| |
| ret, err := h.routeStore.Update(c.Context(), &route, false) |
| if err != nil { |
| return handler.SpecCodeResponse(err), err |
| } |
| |
| return ret, nil |
| } |
| |
| type GetInput struct { |
| ID string `auto_read:"id,path" validate:"required"` |
| } |
| |
| // swagger:operation GET /apisix/admin/routes getRouteList |
| // |
| // Return the route list according to the specified page number and page size, and can search routes by name and uri. |
| // |
| // --- |
| // produces: |
| // - application/json |
| // parameters: |
| // - name: page |
| // in: query |
| // description: page number |
| // required: false |
| // type: integer |
| // - name: page_size |
| // in: query |
| // description: page size |
| // required: false |
| // type: integer |
| // - name: name |
| // in: query |
| // description: name of route |
| // required: false |
| // type: string |
| // - name: uri |
| // in: query |
| // description: uri of route |
| // required: false |
| // type: string |
| // - name: label |
| // in: query |
| // description: label of route |
| // required: false |
| // type: string |
| // responses: |
| // '0': |
| // description: list response |
| // schema: |
| // type: array |
| // items: |
| // "$ref": "#/definitions/route" |
| // default: |
| // description: unexpected error |
| // schema: |
| // "$ref": "#/definitions/ApiError" |
| func (h *Handler) Get(c droplet.Context) (interface{}, error) { |
| input := c.Input().(*GetInput) |
| |
| r, err := h.routeStore.Get(c.Context(), input.ID) |
| if err != nil { |
| return &data.SpecCodeResponse{StatusCode: http.StatusNotFound}, err |
| } |
| |
| //format respond |
| route := r.(*entity.Route) |
| script, _ := h.scriptStore.Get(c.Context(), input.ID) |
| if script != nil { |
| route.Script = script.(*entity.Script).Script |
| } |
| |
| //format |
| if route.Upstream != nil && route.Upstream.Nodes != nil { |
| route.Upstream.Nodes = entity.NodesFormat(route.Upstream.Nodes) |
| } |
| |
| return route, nil |
| } |
| |
| type ListInput struct { |
| Name string `auto_read:"name,query"` |
| URI string `auto_read:"uri,query"` |
| Label string `auto_read:"label,query"` |
| Status string `auto_read:"status,query"` |
| Host string `auto_read:"host,query"` |
| ID string `auto_read:"id,query"` |
| Desc string `auto_read:"desc,query"` |
| store.Pagination |
| } |
| |
| func uriContains(obj *entity.Route, uri string) bool { |
| if strings.Contains(obj.URI, uri) { |
| return true |
| } |
| for _, str := range obj.Uris { |
| result := strings.Contains(str, uri) |
| if result { |
| return true |
| } |
| } |
| return false |
| } |
| |
| func (h *Handler) List(c droplet.Context) (interface{}, error) { |
| input := c.Input().(*ListInput) |
| labelMap, err := utils.GenLabelMap(input.Label) |
| if err != nil { |
| return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, |
| fmt.Errorf("%s: \"%s\"", err.Error(), input.Label) |
| } |
| |
| ret, err := h.routeStore.List(c.Context(), store.ListInput{ |
| Predicate: func(obj interface{}) bool { |
| if input.Name != "" && !strings.Contains(obj.(*entity.Route).Name, input.Name) { |
| return false |
| } |
| |
| if input.URI != "" && !uriContains(obj.(*entity.Route), input.URI) { |
| return false |
| } |
| |
| if input.Label != "" && !utils.LabelContains(obj.(*entity.Route).Labels, labelMap) { |
| return false |
| } |
| |
| if input.Status != "" && strconv.Itoa(int(obj.(*entity.Route).Status)) != input.Status { |
| return false |
| } |
| |
| if input.Host != "" && !strings.Contains(obj.(*entity.Route).Host, input.Host) { |
| return false |
| } |
| |
| if input.Desc != "" && !strings.Contains(obj.(*entity.Route).Desc, input.Desc) { |
| return false |
| } |
| |
| if obj != nil && obj.(*entity.Route) != nil && obj.(*entity.Route).ID != nil && input.ID != "" { |
| if !strings.Contains(utils.InterfaceToString(obj.(*entity.Route).ID), input.ID) { |
| return false // IDs do not match, so object should not be included in the filtered result |
| } |
| } |
| |
| return true |
| }, |
| Format: func(obj interface{}) interface{} { |
| route := obj.(*entity.Route) |
| if route.Upstream != nil && route.Upstream.Nodes != nil { |
| route.Upstream.Nodes = entity.NodesFormat(route.Upstream.Nodes) |
| } |
| return route |
| }, |
| PageSize: input.PageSize, |
| PageNumber: input.PageNumber, |
| }) |
| |
| if err != nil { |
| return nil, err |
| } |
| |
| //format respond |
| var route *entity.Route |
| for i, item := range ret.Rows { |
| route = item.(*entity.Route) |
| id := utils.InterfaceToString(route.ID) |
| script, _ := h.scriptStore.Get(c.Context(), id) |
| if script != nil { |
| route.Script = script.(*entity.Script).Script |
| } |
| ret.Rows[i] = route |
| } |
| |
| return ret, nil |
| } |
| |
| func generateLuaCode(script map[string]interface{}) (string, error) { |
| scriptString, err := json.Marshal(script) |
| if err != nil { |
| return "", err |
| } |
| workDir, err := filepath.Abs(conf.WorkDir) |
| if err != nil { |
| return "", err |
| } |
| libDir := filepath.Join(workDir, "dag-to-lua/") |
| if err := os.Chdir(libDir); err != nil { |
| log.Errorf("Chdir to libDir failed: %s", err) |
| return "", err |
| } |
| |
| defer func() { |
| if err := os.Chdir(workDir); err != nil { |
| log.Errorf("Chdir to workDir failed: %s", err) |
| } |
| }() |
| |
| L := lua.NewState() |
| defer L.Close() |
| |
| if err := L.DoString(` |
| local dag_to_lua = require 'dag-to-lua' |
| local conf = [==[` + string(scriptString) + `]==] |
| code = dag_to_lua.generate(conf) |
| `); err != nil { |
| return "", err |
| } |
| |
| code := L.GetGlobal("code") |
| |
| return code.String(), nil |
| } |
| |
| func (h *Handler) Create(c droplet.Context) (interface{}, error) { |
| input := c.Input().(*entity.Route) |
| //check depend |
| if input.ServiceID != nil { |
| serviceID := utils.InterfaceToString(input.ServiceID) |
| _, err := h.svcStore.Get(c.Context(), serviceID) |
| if err != nil { |
| if err == data.ErrNotFound { |
| return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, |
| fmt.Errorf(consts.IDNotFound, "service", input.ServiceID) |
| } |
| return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err |
| } |
| } |
| if input.UpstreamID != nil { |
| upstreamID := utils.InterfaceToString(input.UpstreamID) |
| _, err := h.upstreamStore.Get(c.Context(), upstreamID) |
| if err != nil { |
| if err == data.ErrNotFound { |
| return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, |
| fmt.Errorf("upstream id: %s not found", input.UpstreamID) |
| } |
| return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err |
| } |
| } |
| |
| // If route's script_id is set, it must be equals to the route's id. |
| if input.ScriptID != nil && (utils.InterfaceToString(input.ID) != utils.InterfaceToString(input.ScriptID)) { |
| return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, |
| fmt.Errorf("script_id must be the same as id") |
| } |
| |
| if input.Script != nil { |
| if utils.InterfaceToString(input.ID) == "" { |
| input.ID = utils.GetFlakeUidStr() |
| } |
| script := &entity.Script{} |
| script.ID = utils.InterfaceToString(input.ID) |
| script.Script = input.Script |
| |
| var err error |
| // Explicitly to lua if input script is of the map type, otherwise |
| // it will always represent a piece of lua code of the string type. |
| if scriptConf, ok := input.Script.(map[string]interface{}); ok { |
| // For lua code of map type, syntax validation is done by |
| // the generateLuaCode function |
| input.Script, err = generateLuaCode(scriptConf) |
| if err != nil { |
| return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err |
| } |
| } else { |
| // For lua code of string type, use utility func to syntax validation |
| err = utils.ValidateLuaCode(input.Script.(string)) |
| if err != nil { |
| return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err |
| } |
| } |
| |
| //save original conf |
| if _, err = h.scriptStore.Create(c.Context(), script); err != nil { |
| return nil, err |
| } |
| |
| // After saving the Script entity, always set route's script_id |
| // the same as route's id. |
| input.ScriptID = input.ID |
| } else { |
| // If script is unset, script_id must be unset neither. |
| if input.ScriptID != nil { |
| return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, |
| fmt.Errorf("script_id cannot be set if script is unset") |
| } |
| } |
| |
| // check name existed |
| ret, err := handler.NameExistCheck(c.Context(), h.routeStore, "route", input.Name, nil) |
| if err != nil { |
| return ret, err |
| } |
| |
| // create |
| res, err := h.routeStore.Create(c.Context(), input) |
| if err != nil { |
| return handler.SpecCodeResponse(err), err |
| } |
| |
| return res, nil |
| } |
| |
| type UpdateInput struct { |
| ID string `auto_read:"id,path"` |
| entity.Route |
| } |
| |
| func (h *Handler) Update(c droplet.Context) (interface{}, error) { |
| input := c.Input().(*UpdateInput) |
| |
| // check if ID in body is equal ID in path |
| if err := handler.IDCompare(input.ID, input.Route.ID); err != nil { |
| return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err |
| } |
| |
| // if has id in path, use it |
| if input.ID != "" { |
| input.Route.ID = input.ID |
| } |
| |
| //check depend |
| if input.ServiceID != nil { |
| serviceID := utils.InterfaceToString(input.ServiceID) |
| _, err := h.svcStore.Get(c.Context(), serviceID) |
| if err != nil { |
| if err == data.ErrNotFound { |
| return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, |
| fmt.Errorf(consts.IDNotFound, "service", input.ServiceID) |
| } |
| return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err |
| } |
| } |
| if input.UpstreamID != nil { |
| upstreamID := utils.InterfaceToString(input.UpstreamID) |
| _, err := h.upstreamStore.Get(c.Context(), upstreamID) |
| if err != nil { |
| if err == data.ErrNotFound { |
| return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, |
| fmt.Errorf("upstream id: %s not found", input.UpstreamID) |
| } |
| return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err |
| } |
| } |
| |
| // If route's script_id is set, it must be equals to the route's id. |
| if input.Route.ScriptID != nil && (utils.InterfaceToString(input.ID) != utils.InterfaceToString(input.Route.ScriptID)) { |
| return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, |
| fmt.Errorf("script_id must be the same as id") |
| } |
| |
| if input.Script != nil { |
| script := &entity.Script{} |
| script.ID = input.ID |
| script.Script = input.Script |
| |
| var err error |
| // Explicitly to lua if input script is of the map type, otherwise |
| // it will always represent a piece of lua code of the string type. |
| if scriptConf, ok := input.Script.(map[string]interface{}); ok { |
| // For lua code of map type, syntax validation is done by |
| // the generateLuaCode function |
| input.Route.Script, err = generateLuaCode(scriptConf) |
| if err != nil { |
| return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err |
| } |
| } else { |
| // For lua code of string type, use utility func to syntax validation |
| err = utils.ValidateLuaCode(input.Script.(string)) |
| if err != nil { |
| return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, err |
| } |
| } |
| |
| //save original conf |
| if _, err = h.scriptStore.Update(c.Context(), script, true); err != nil { |
| //if not exists, create |
| if err.Error() == fmt.Sprintf("key: %s is not found", script.ID) { |
| if _, err := h.scriptStore.Create(c.Context(), script); err != nil { |
| return handler.SpecCodeResponse(err), err |
| } |
| } else { |
| return handler.SpecCodeResponse(err), err |
| } |
| } |
| |
| // After updating the Script entity, always set route's script_id |
| // the same as route's id. |
| input.Route.ScriptID = input.ID |
| } else { |
| // If script is unset, script_id must be unset neither. |
| if input.Route.ScriptID != nil { |
| return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, |
| fmt.Errorf("script_id cannot be set if script is unset") |
| } |
| //remove exists script |
| id := utils.InterfaceToString(input.Route.ID) |
| script, _ := h.scriptStore.Get(c.Context(), id) |
| if script != nil { |
| if err := h.scriptStore.BatchDelete(c.Context(), strings.Split(id, ",")); err != nil { |
| log.Warnf("delete script %s failed", input.Route.ID) |
| } |
| } |
| } |
| |
| // check name existed |
| ret, err := handler.NameExistCheck(c.Context(), h.routeStore, "route", input.Name, input.ID) |
| if err != nil { |
| return ret, err |
| } |
| |
| // create |
| res, err := h.routeStore.Update(c.Context(), &input.Route, true) |
| if err != nil { |
| return handler.SpecCodeResponse(err), err |
| } |
| |
| return res, nil |
| } |
| |
| type BatchDelete struct { |
| IDs string `auto_read:"ids,path"` |
| } |
| |
| func (h *Handler) BatchDelete(c droplet.Context) (interface{}, error) { |
| input := c.Input().(*BatchDelete) |
| |
| //delete route |
| if err := h.routeStore.BatchDelete(c.Context(), strings.Split(input.IDs, ",")); err != nil { |
| return handler.SpecCodeResponse(err), err |
| } |
| |
| //delete stored script |
| if err := h.scriptStore.BatchDelete(c.Context(), strings.Split(input.IDs, ",")); err != nil { |
| //try again |
| log.Warn("try to delete script %s again", input.IDs) |
| if err := h.scriptStore.BatchDelete(c.Context(), strings.Split(input.IDs, ",")); err != nil { |
| return nil, nil |
| } |
| } |
| |
| return nil, nil |
| } |
| |
| type ExistCheckInput struct { |
| Name string `auto_read:"name,query"` |
| Exclude string `auto_read:"exclude,query"` |
| } |
| |
| // swagger:operation GET /apisix/admin/notexist/routes checkRouteExist |
| // |
| // Return result of route exists checking by name and exclude id. |
| // |
| // --- |
| // produces: |
| // - application/json |
| // parameters: |
| // - name: name |
| // in: query |
| // description: name of route |
| // required: false |
| // type: string |
| // - name: exclude |
| // in: query |
| // description: id of route that exclude checking |
| // required: false |
| // type: string |
| // responses: |
| // '0': |
| // description: route not exists |
| // schema: |
| // "$ref": "#/definitions/ApiError" |
| // default: |
| // description: unexpected error |
| // schema: |
| // "$ref": "#/definitions/ApiError" |
| func (h *Handler) Exist(c droplet.Context) (interface{}, error) { |
| input := c.Input().(*ExistCheckInput) |
| name := input.Name |
| exclude := input.Exclude |
| |
| ret, err := h.routeStore.List(c.Context(), store.ListInput{ |
| Predicate: func(obj interface{}) bool { |
| r := obj.(*entity.Route) |
| if r.Name == name && r.ID != exclude { |
| return true |
| } |
| |
| return false |
| }, |
| PageSize: 0, |
| PageNumber: 0, |
| }) |
| if err != nil { |
| return nil, err |
| } |
| |
| if ret.TotalSize > 0 { |
| return &data.SpecCodeResponse{StatusCode: http.StatusBadRequest}, |
| consts.InvalidParam("Route name is reduplicate") |
| } |
| |
| return nil, nil |
| } |