1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-08-02 20:35:25 +02:00

fix(performance): optimize performance for edge EE-3311 (#8040)

This commit is contained in:
andres-portainer 2023-01-06 16:25:41 -03:00 committed by GitHub
parent 3d28a6f877
commit dd0d1737b0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 577 additions and 164 deletions

View file

@ -1,17 +1,23 @@
package endpointedge
import (
"bytes"
"encoding/base64"
"errors"
"fmt"
"hash/fnv"
"io"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"time"
httperror "github.com/portainer/libhttp/error"
"github.com/portainer/libhttp/request"
"github.com/portainer/libhttp/response"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/http/middlewares"
"github.com/portainer/portainer/api/internal/edge/cache"
)
type stackStatusResponse struct {
@ -64,9 +70,27 @@ type endpointEdgeStatusInspectResponse struct {
// @failure 500 "Server error"
// @router /endpoints/{id}/edge/status [get]
func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
endpoint, err := middlewares.FetchEndpoint(r)
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
if err != nil {
return httperror.BadRequest("Unable to find an environment on request context", err)
return httperror.BadRequest("Invalid environment identifier route variable", err)
}
cachedResp := handler.respondFromCache(w, r, portainer.EndpointID(endpointID))
if cachedResp {
return nil
}
if _, ok := handler.DataStore.Endpoint().Heartbeat(portainer.EndpointID(endpointID)); !ok {
return httperror.NotFound("Unable to find an environment with the specified identifier inside the database", nil)
}
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
if err != nil {
if handler.DataStore.IsErrObjectNotFound(err) {
return httperror.NotFound("Unable to find an environment with the specified identifier inside the database", err)
}
return httperror.InternalServerError("Unable to find an environment with the specified identifier inside the database", err)
}
err = handler.requestBouncer.AuthorizedEdgeEndpointOperation(r, endpoint)
@ -129,7 +153,7 @@ func (handler *Handler) endpointEdgeStatusInspect(w http.ResponseWriter, r *http
}
statusResponse.Stacks = edgeStacksStatus
return response.JSON(w, statusResponse)
return cacheResponse(w, endpoint.ID, statusResponse)
}
func parseAgentPlatform(r *http.Request) (portainer.EndpointType, error) {
@ -191,17 +215,75 @@ func (handler *Handler) buildEdgeStacks(endpointID portainer.EndpointID) ([]stac
edgeStacksStatus := []stackStatusResponse{}
for stackID := range relation.EdgeStacks {
stack, err := handler.DataStore.EdgeStack().EdgeStack(stackID)
if err != nil {
version, ok := handler.DataStore.EdgeStack().EdgeStackVersion(stackID)
if !ok {
return nil, httperror.InternalServerError("Unable to retrieve edge stack from the database", err)
}
stackStatus := stackStatusResponse{
ID: stack.ID,
Version: stack.Version,
ID: stackID,
Version: version,
}
edgeStacksStatus = append(edgeStacksStatus, stackStatus)
}
return edgeStacksStatus, nil
}
func cacheResponse(w http.ResponseWriter, endpointID portainer.EndpointID, statusResponse endpointEdgeStatusInspectResponse) *httperror.HandlerError {
rr := httptest.NewRecorder()
httpErr := response.JSON(rr, statusResponse)
if httpErr != nil {
return httpErr
}
h := fnv.New32a()
h.Write(rr.Body.Bytes())
etag := strconv.FormatUint(uint64(h.Sum32()), 16)
cache.Set(endpointID, []byte(etag))
resp := rr.Result()
for k, vs := range resp.Header {
for _, v := range vs {
w.Header().Add(k, v)
}
}
w.Header().Set("ETag", etag)
io.Copy(w, resp.Body)
return nil
}
func (handler *Handler) respondFromCache(w http.ResponseWriter, r *http.Request, endpointID portainer.EndpointID) bool {
inmHeader := r.Header.Get("If-None-Match")
etags := strings.Split(inmHeader, ",")
if len(inmHeader) == 0 || etags[0] == "" {
return false
}
cachedETag, ok := cache.Get(endpointID)
if !ok {
return false
}
for _, etag := range etags {
if !bytes.Equal([]byte(etag), cachedETag) {
continue
}
handler.DataStore.Endpoint().UpdateHeartbeat(endpointID)
w.Header().Set("ETag", etag)
w.WriteHeader(http.StatusNotModified)
return true
}
return false
}

View file

@ -158,7 +158,7 @@ func TestMissingEdgeIdentifier(t *testing.T) {
t.Fatal(err)
}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpointID), nil)
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/endpoints/%d/edge/status", endpointID), nil)
if err != nil {
t.Fatal("request error:", err)
}
@ -185,7 +185,7 @@ func TestWithEndpoints(t *testing.T) {
t.Fatal(err)
}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", test.endpoint.ID), nil)
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/endpoints/%d/edge/status", test.endpoint.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
@ -231,7 +231,7 @@ func TestLastCheckInDateIncreases(t *testing.T) {
time.Sleep(1 * time.Second)
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/endpoints/%d/edge/status", endpoint.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
@ -279,7 +279,7 @@ func TestEmptyEdgeIdWithAgentPlatformHeader(t *testing.T) {
t.Fatal(err)
}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/endpoints/%d/edge/status", endpoint.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
@ -348,7 +348,7 @@ func TestEdgeStackStatus(t *testing.T) {
t.Fatal(err)
}
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/endpoints/%d/edge/status", endpoint.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}
@ -418,7 +418,7 @@ func TestEdgeJobsResponse(t *testing.T) {
handler.ReverseTunnelService.AddEdgeJob(endpoint.ID, &edgeJob)
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/%d/edge/status", endpoint.ID), nil)
req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("/api/endpoints/%d/edge/status", endpoint.ID), nil)
if err != nil {
t.Fatal("request error:", err)
}

View file

@ -31,14 +31,16 @@ func NewHandler(bouncer *security.RequestBouncer, dataStore dataservices.DataSto
ReverseTunnelService: reverseTunnelService,
}
endpointRouter := h.PathPrefix("/{id}").Subrouter()
h.Handle("/api/endpoints/{id}/edge/status", bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeStatusInspect))).Methods(http.MethodGet)
endpointRouter := h.PathPrefix("/api/endpoints/{id}").Subrouter()
endpointRouter.Use(middlewares.WithEndpoint(dataStore.Endpoint(), "id"))
endpointRouter.PathPrefix("/edge/status").Handler(
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeStatusInspect))).Methods(http.MethodGet)
endpointRouter.PathPrefix("/edge/stacks/{stackId}").Handler(
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeStackInspect))).Methods(http.MethodGet)
endpointRouter.PathPrefix("/edge/jobs/{jobID}/logs").Handler(
bouncer.PublicAccess(httperror.LoggerHandler(h.endpointEdgeJobsLogs))).Methods(http.MethodPost)
return h
}