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:
parent
76d1b70644
commit
e217ac7121
44 changed files with 1099 additions and 307 deletions
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
113
api/http/handler/endpoints/endpoint_list_test.go
Normal file
113
api/http/handler/endpoints/endpoint_list_test.go
Normal 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(®ularEndpoint)
|
||||
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
|
||||
}
|
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue