1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-25 00:09:40 +02:00

feat(edge): show correct heartbeat and sync aeec changes [EE-2876] (#6769)

This commit is contained in:
Chaim Lev-Ari 2022-04-19 21:43:36 +03:00 committed by GitHub
parent 76d1b70644
commit e217ac7121
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 1099 additions and 307 deletions

View file

@ -326,6 +326,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
EdgeCheckinInterval: payload.EdgeCheckinInterval,
Kubernetes: portainer.KubernetesDefault(),
IsEdgeDevice: payload.IsEdgeDevice,
UserTrusted: true,
}
settings, err := handler.DataStore.Settings().Settings()

View file

@ -14,6 +14,12 @@ import (
"github.com/portainer/portainer/api/http/security"
)
const (
EdgeDeviceFilterAll = "all"
EdgeDeviceFilterTrusted = "trusted"
EdgeDeviceFilterUntrusted = "untrusted"
)
// @id EndpointList
// @summary List environments(endpoints)
// @description List all environments(endpoints) based on the current user authorizations. Will
@ -32,6 +38,7 @@ import (
// @param tagIds query []int false "search environments(endpoints) with these tags (depends on tagsPartialMatch)"
// @param tagsPartialMatch query bool false "If true, will return environment(endpoint) which has one of tagIds, if false (or missing) will return only environments(endpoints) that has all the tags"
// @param endpointIds query []int false "will return only these environments(endpoints)"
// @param edgeDeviceFilter query string false "will return only these edge devices" Enum("all", "trusted", "untrusted")
// @success 200 {array} portainer.Endpoint "Endpoints"
// @failure 500 "Server error"
// @router /endpoints [get]
@ -91,8 +98,8 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
filteredEndpoints = filterEndpointsByGroupID(filteredEndpoints, portainer.EndpointGroupID(groupID))
}
edgeDeviceFilter, edgeDeviceFilterErr := request.RetrieveBooleanQueryParameter(r, "edgeDeviceFilter", false)
if edgeDeviceFilterErr == nil {
edgeDeviceFilter, _ := request.RetrieveQueryParameter(r, "edgeDeviceFilter", false)
if edgeDeviceFilter != "" {
filteredEndpoints = filterEndpointsByEdgeDevice(filteredEndpoints, edgeDeviceFilter)
}
@ -240,17 +247,38 @@ func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []int)
return filteredEndpoints
}
func filterEndpointsByEdgeDevice(endpoints []portainer.Endpoint, edgeDeviceFilter bool) []portainer.Endpoint {
func filterEndpointsByEdgeDevice(endpoints []portainer.Endpoint, edgeDeviceFilter string) []portainer.Endpoint {
filteredEndpoints := make([]portainer.Endpoint, 0)
if edgeDeviceFilter != EdgeDeviceFilterAll && edgeDeviceFilter != EdgeDeviceFilterTrusted && edgeDeviceFilter != EdgeDeviceFilterUntrusted {
return endpoints
}
for _, endpoint := range endpoints {
if edgeDeviceFilter == endpoint.IsEdgeDevice {
if shouldReturnEdgeDevice(endpoint, edgeDeviceFilter) {
filteredEndpoints = append(filteredEndpoints, endpoint)
}
}
return filteredEndpoints
}
func shouldReturnEdgeDevice(endpoint portainer.Endpoint, edgeDeviceFilter string) bool {
if !endpoint.IsEdgeDevice {
return false
}
switch edgeDeviceFilter {
case EdgeDeviceFilterAll:
return true
case EdgeDeviceFilterTrusted:
return endpoint.UserTrusted
case EdgeDeviceFilterUntrusted:
return !endpoint.UserTrusted
}
return false
}
func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string {
tags := make([]string, 0)
for _, tagID := range tagIDs {

View file

@ -0,0 +1,113 @@
package endpoints
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/datastore"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/testhelpers"
helper "github.com/portainer/portainer/api/internal/testhelpers"
"github.com/stretchr/testify/assert"
)
type endpointListEdgeDeviceTest struct {
expected []portainer.EndpointID
filter string
}
func Test_endpointList(t *testing.T) {
is := assert.New(t)
_, store, teardown := datastore.MustNewTestStore(true, true)
defer teardown()
trustedEndpoint := portainer.Endpoint{ID: 1, UserTrusted: true, IsEdgeDevice: true, GroupID: 1}
err := store.Endpoint().Create(&trustedEndpoint)
is.NoError(err, "error creating environment")
untrustedEndpoint := portainer.Endpoint{ID: 2, UserTrusted: false, IsEdgeDevice: true, GroupID: 1}
err = store.Endpoint().Create(&untrustedEndpoint)
is.NoError(err, "error creating environment")
regularEndpoint := portainer.Endpoint{ID: 3, IsEdgeDevice: false, GroupID: 1}
err = store.Endpoint().Create(&regularEndpoint)
is.NoError(err, "error creating environment")
err = store.User().Create(&portainer.User{Username: "admin", Role: portainer.AdministratorRole})
is.NoError(err, "error creating a user")
bouncer := helper.NewTestRequestBouncer()
h := NewHandler(bouncer)
h.DataStore = store
h.ComposeStackManager = testhelpers.NewComposeStackManager()
tests := []endpointListEdgeDeviceTest{
{
[]portainer.EndpointID{trustedEndpoint.ID, untrustedEndpoint.ID},
EdgeDeviceFilterAll,
},
{
[]portainer.EndpointID{trustedEndpoint.ID},
EdgeDeviceFilterTrusted,
},
{
[]portainer.EndpointID{untrustedEndpoint.ID},
EdgeDeviceFilterUntrusted,
},
}
for _, test := range tests {
req := buildEndpointListRequest(test.filter)
resp, err := doEndpointListRequest(req, h, is)
is.NoError(err)
is.Equal(len(test.expected), len(resp))
respIds := []portainer.EndpointID{}
for _, endpoint := range resp {
respIds = append(respIds, endpoint.ID)
}
is.Equal(test.expected, respIds, "response should contain all edge devices")
}
}
func buildEndpointListRequest(filter string) *http.Request {
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/endpoints?edgeDeviceFilter=%s", filter), nil)
ctx := security.StoreTokenData(req, &portainer.TokenData{ID: 1, Username: "admin", Role: 1})
req = req.WithContext(ctx)
restrictedCtx := security.StoreRestrictedRequestContext(req, &security.RestrictedRequestContext{UserID: 1, IsAdmin: true})
req = req.WithContext(restrictedCtx)
req.Header.Add("Authorization", "Bearer dummytoken")
return req
}
func doEndpointListRequest(req *http.Request, h *Handler, is *assert.Assertions) ([]portainer.Endpoint, error) {
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
is.Equal(http.StatusOK, rr.Code, "Status should be 200")
body, err := io.ReadAll(rr.Body)
if err != nil {
return nil, err
}
resp := []portainer.Endpoint{}
err = json.Unmarshal(body, &resp)
if err != nil {
return nil, err
}
return resp, nil
}

View file

@ -46,8 +46,6 @@ type endpointUpdatePayload struct {
EdgeCheckinInterval *int `example:"5"`
// Associated Kubernetes data
Kubernetes *portainer.KubernetesData
// Whether the device has been trusted or not by the user
UserTrusted *bool
}
func (payload *endpointUpdatePayload) Validate(r *http.Request) error {
@ -273,10 +271,6 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
}
}
if payload.UserTrusted != nil {
endpoint.UserTrusted = *payload.UserTrusted
}
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
if err != nil {
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist environment changes inside the database", err}

View file

@ -5,7 +5,6 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/http/proxy"
"github.com/portainer/portainer/api/http/security"
"github.com/portainer/portainer/api/internal/authorization"
"github.com/portainer/portainer/api/kubernetes/cli"
@ -21,10 +20,21 @@ func hideFields(endpoint *portainer.Endpoint) {
}
}
// This requestBouncer exists because security.RequestBounder is a type and not an interface.
// Therefore we can not swit it out with a dummy bouncer for go tests. This interface works around it
type requestBouncer interface {
AuthenticatedAccess(h http.Handler) http.Handler
AdminAccess(h http.Handler) http.Handler
RestrictedAccess(h http.Handler) http.Handler
PublicAccess(h http.Handler) http.Handler
AuthorizedEndpointOperation(r *http.Request, endpoint *portainer.Endpoint) error
AuthorizedEdgeEndpointOperation(r *http.Request, endpoint *portainer.Endpoint) error
}
// Handler is the HTTP handler used to handle environment(endpoint) operations.
type Handler struct {
*mux.Router
requestBouncer *security.RequestBouncer
requestBouncer requestBouncer
DataStore dataservices.DataStore
FileService portainer.FileService
ProxyManager *proxy.Manager
@ -38,7 +48,7 @@ type Handler struct {
}
// NewHandler creates a handler to manage environment(endpoint) operations.
func NewHandler(bouncer *security.RequestBouncer) *Handler {
func NewHandler(bouncer requestBouncer) *Handler {
h := &Handler{
Router: mux.NewRouter(),
requestBouncer: bouncer,

View file

@ -43,8 +43,8 @@ type settingsUpdatePayload struct {
HelmRepositoryURL *string `example:"https://charts.bitnami.com/bitnami"`
// Kubectl Shell Image
KubectlShellImage *string `example:"portainer/kubectl-shell:latest"`
// DisableTrustOnFirstConnect makes Portainer require explicit user trust of the edge agent before accepting the connection
DisableTrustOnFirstConnect *bool `example:"false"`
// TrustOnFirstConnect makes Portainer accepting edge agent connection by default
TrustOnFirstConnect *bool `example:"false"`
// EnforceEdgeID makes Portainer store the Edge ID instead of accepting anyone
EnforceEdgeID *bool `example:"false"`
// EdgePortainerURL is the URL that is exposed to edge agents
@ -180,8 +180,8 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
settings.EnableEdgeComputeFeatures = *payload.EnableEdgeComputeFeatures
}
if payload.DisableTrustOnFirstConnect != nil {
settings.DisableTrustOnFirstConnect = *payload.DisableTrustOnFirstConnect
if payload.TrustOnFirstConnect != nil {
settings.TrustOnFirstConnect = *payload.TrustOnFirstConnect
}
if payload.EnforceEdgeID != nil {

View file

@ -144,7 +144,7 @@ func (bouncer *RequestBouncer) AuthorizedEdgeEndpointOperation(r *http.Request,
return fmt.Errorf("could not retrieve the settings: %w", err)
}
if settings.DisableTrustOnFirstConnect {
if !settings.TrustOnFirstConnect {
return errors.New("the device has not been trusted yet")
}