mirror of
https://github.com/portainer/portainer.git
synced 2025-07-31 11:19:40 +02:00
fix(edgegroups): convert the related endpoint IDs to roaring bitmaps to increase performance BE-12053 (#903)
This commit is contained in:
parent
caf382b64c
commit
937456596a
32 changed files with 1041 additions and 133 deletions
|
@ -17,11 +17,29 @@ func (service ServiceTx) UpdateEdgeGroupFunc(ID portainer.EdgeGroupID, updateFun
|
||||||
}
|
}
|
||||||
|
|
||||||
func (service ServiceTx) Create(group *portainer.EdgeGroup) error {
|
func (service ServiceTx) Create(group *portainer.EdgeGroup) error {
|
||||||
return service.Tx.CreateObject(
|
es := group.Endpoints
|
||||||
|
group.Endpoints = nil // Clear deprecated field
|
||||||
|
|
||||||
|
err := service.Tx.CreateObject(
|
||||||
BucketName,
|
BucketName,
|
||||||
func(id uint64) (int, any) {
|
func(id uint64) (int, any) {
|
||||||
group.ID = portainer.EdgeGroupID(id)
|
group.ID = portainer.EdgeGroupID(id)
|
||||||
return int(group.ID), group
|
return int(group.ID), group
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
group.Endpoints = es // Restore endpoints after create
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (service ServiceTx) Update(ID portainer.EdgeGroupID, group *portainer.EdgeGroup) error {
|
||||||
|
es := group.Endpoints
|
||||||
|
group.Endpoints = nil // Clear deprecated field
|
||||||
|
|
||||||
|
err := service.BaseDataServiceTx.Update(ID, group)
|
||||||
|
|
||||||
|
group.Endpoints = es // Restore endpoints after update
|
||||||
|
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,6 +85,7 @@ func (store *Store) newMigratorParameters(version *models.Version, flags *portai
|
||||||
EdgeStackService: store.EdgeStackService,
|
EdgeStackService: store.EdgeStackService,
|
||||||
EdgeStackStatusService: store.EdgeStackStatusService,
|
EdgeStackStatusService: store.EdgeStackStatusService,
|
||||||
EdgeJobService: store.EdgeJobService,
|
EdgeJobService: store.EdgeJobService,
|
||||||
|
EdgeGroupService: store.EdgeGroupService,
|
||||||
TunnelServerService: store.TunnelServerService,
|
TunnelServerService: store.TunnelServerService,
|
||||||
PendingActionsService: store.PendingActionsService,
|
PendingActionsService: store.PendingActionsService,
|
||||||
}
|
}
|
||||||
|
|
23
api/datastore/migrator/migrate_2_33_0.go
Normal file
23
api/datastore/migrator/migrate_2_33_0.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package migrator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/portainer/portainer/api/roar"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (m *Migrator) migrateEdgeGroupEndpointsToRoars_2_33_0() error {
|
||||||
|
egs, err := m.edgeGroupService.ReadAll()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, eg := range egs {
|
||||||
|
eg.EndpointIDs = roar.FromSlice(eg.Endpoints)
|
||||||
|
eg.Endpoints = nil
|
||||||
|
|
||||||
|
if err := m.edgeGroupService.Update(eg.ID, &eg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -6,6 +6,7 @@ import (
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/database/models"
|
"github.com/portainer/portainer/api/database/models"
|
||||||
"github.com/portainer/portainer/api/dataservices/dockerhub"
|
"github.com/portainer/portainer/api/dataservices/dockerhub"
|
||||||
|
"github.com/portainer/portainer/api/dataservices/edgegroup"
|
||||||
"github.com/portainer/portainer/api/dataservices/edgejob"
|
"github.com/portainer/portainer/api/dataservices/edgejob"
|
||||||
"github.com/portainer/portainer/api/dataservices/edgestack"
|
"github.com/portainer/portainer/api/dataservices/edgestack"
|
||||||
"github.com/portainer/portainer/api/dataservices/edgestackstatus"
|
"github.com/portainer/portainer/api/dataservices/edgestackstatus"
|
||||||
|
@ -60,6 +61,7 @@ type (
|
||||||
edgeStackService *edgestack.Service
|
edgeStackService *edgestack.Service
|
||||||
edgeStackStatusService *edgestackstatus.Service
|
edgeStackStatusService *edgestackstatus.Service
|
||||||
edgeJobService *edgejob.Service
|
edgeJobService *edgejob.Service
|
||||||
|
edgeGroupService *edgegroup.Service
|
||||||
TunnelServerService *tunnelserver.Service
|
TunnelServerService *tunnelserver.Service
|
||||||
pendingActionsService *pendingactions.Service
|
pendingActionsService *pendingactions.Service
|
||||||
}
|
}
|
||||||
|
@ -89,6 +91,7 @@ type (
|
||||||
EdgeStackService *edgestack.Service
|
EdgeStackService *edgestack.Service
|
||||||
EdgeStackStatusService *edgestackstatus.Service
|
EdgeStackStatusService *edgestackstatus.Service
|
||||||
EdgeJobService *edgejob.Service
|
EdgeJobService *edgejob.Service
|
||||||
|
EdgeGroupService *edgegroup.Service
|
||||||
TunnelServerService *tunnelserver.Service
|
TunnelServerService *tunnelserver.Service
|
||||||
PendingActionsService *pendingactions.Service
|
PendingActionsService *pendingactions.Service
|
||||||
}
|
}
|
||||||
|
@ -120,11 +123,13 @@ func NewMigrator(parameters *MigratorParameters) *Migrator {
|
||||||
edgeStackService: parameters.EdgeStackService,
|
edgeStackService: parameters.EdgeStackService,
|
||||||
edgeStackStatusService: parameters.EdgeStackStatusService,
|
edgeStackStatusService: parameters.EdgeStackStatusService,
|
||||||
edgeJobService: parameters.EdgeJobService,
|
edgeJobService: parameters.EdgeJobService,
|
||||||
|
edgeGroupService: parameters.EdgeGroupService,
|
||||||
TunnelServerService: parameters.TunnelServerService,
|
TunnelServerService: parameters.TunnelServerService,
|
||||||
pendingActionsService: parameters.PendingActionsService,
|
pendingActionsService: parameters.PendingActionsService,
|
||||||
}
|
}
|
||||||
|
|
||||||
migrator.initMigrations()
|
migrator.initMigrations()
|
||||||
|
|
||||||
return migrator
|
return migrator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,6 +256,8 @@ func (m *Migrator) initMigrations() {
|
||||||
|
|
||||||
m.addMigrations("2.32.0", m.addEndpointRelationForEdgeAgents_2_32_0)
|
m.addMigrations("2.32.0", m.addEndpointRelationForEdgeAgents_2_32_0)
|
||||||
|
|
||||||
|
m.addMigrations("2.33.0", m.migrateEdgeGroupEndpointsToRoars_2_33_0)
|
||||||
|
|
||||||
// Add new migrations above...
|
// Add new migrations above...
|
||||||
// One function per migration, each versions migration funcs in the same file.
|
// One function per migration, each versions migration funcs in the same file.
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||||
|
"github.com/portainer/portainer/api/roar"
|
||||||
)
|
)
|
||||||
|
|
||||||
type endpointSetType map[portainer.EndpointID]bool
|
type endpointSetType map[portainer.EndpointID]bool
|
||||||
|
@ -49,22 +50,29 @@ func GetEndpointsByTags(tx dataservices.DataStoreTx, tagIDs []portainer.TagID, p
|
||||||
return results, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getTrustedEndpoints(tx dataservices.DataStoreTx, endpointIDs []portainer.EndpointID) ([]portainer.EndpointID, error) {
|
func getTrustedEndpoints(tx dataservices.DataStoreTx, endpointIDs roar.Roar[portainer.EndpointID]) ([]portainer.EndpointID, error) {
|
||||||
|
var innerErr error
|
||||||
|
|
||||||
results := []portainer.EndpointID{}
|
results := []portainer.EndpointID{}
|
||||||
for _, endpointID := range endpointIDs {
|
|
||||||
|
endpointIDs.Iterate(func(endpointID portainer.EndpointID) bool {
|
||||||
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
innerErr = err
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !endpoint.UserTrusted {
|
if !endpoint.UserTrusted {
|
||||||
continue
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
results = append(results, endpoint.ID)
|
results = append(results, endpoint.ID)
|
||||||
}
|
|
||||||
|
|
||||||
return results, nil
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return results, innerErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapEndpointGroupToEndpoints(endpoints []portainer.Endpoint) map[portainer.EndpointGroupID]endpointSetType {
|
func mapEndpointGroupToEndpoints(endpoints []portainer.Endpoint) map[portainer.EndpointGroupID]endpointSetType {
|
||||||
|
|
|
@ -7,6 +7,7 @@ import (
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||||
|
"github.com/portainer/portainer/api/roar"
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
)
|
)
|
||||||
|
@ -52,6 +53,7 @@ func calculateEndpointsOrTags(tx dataservices.DataStoreTx, edgeGroup *portainer.
|
||||||
}
|
}
|
||||||
|
|
||||||
edgeGroup.Endpoints = endpointIDs
|
edgeGroup.Endpoints = endpointIDs
|
||||||
|
edgeGroup.EndpointIDs = roar.FromSlice(endpointIDs)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -94,6 +96,7 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request)
|
||||||
Dynamic: payload.Dynamic,
|
Dynamic: payload.Dynamic,
|
||||||
TagIDs: []portainer.TagID{},
|
TagIDs: []portainer.TagID{},
|
||||||
Endpoints: []portainer.EndpointID{},
|
Endpoints: []portainer.EndpointID{},
|
||||||
|
EndpointIDs: roar.Roar[portainer.EndpointID]{},
|
||||||
PartialMatch: payload.PartialMatch,
|
PartialMatch: payload.PartialMatch,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,5 +111,5 @@ func (handler *Handler) edgeGroupCreate(w http.ResponseWriter, r *http.Request)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
return txResponse(w, edgeGroup, err)
|
return txResponse(w, shadowedEdgeGroup{EdgeGroup: *edgeGroup}, err)
|
||||||
}
|
}
|
||||||
|
|
62
api/http/handler/edgegroups/edgegroup_create_test.go
Normal file
62
api/http/handler/edgegroups/edgegroup_create_test.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package edgegroups
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/datastore"
|
||||||
|
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||||
|
|
||||||
|
"github.com/segmentio/encoding/json"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEdgeGroupCreateHandler(t *testing.T) {
|
||||||
|
_, store := datastore.MustNewTestStore(t, true, true)
|
||||||
|
|
||||||
|
handler := NewHandler(testhelpers.NewTestRequestBouncer())
|
||||||
|
handler.DataStore = store
|
||||||
|
|
||||||
|
err := store.EndpointGroup().Create(&portainer.EndpointGroup{
|
||||||
|
ID: 1,
|
||||||
|
Name: "Test Group",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for i := range 3 {
|
||||||
|
err = store.Endpoint().Create(&portainer.Endpoint{
|
||||||
|
ID: portainer.EndpointID(i + 1),
|
||||||
|
Name: "Test Endpoint " + strconv.Itoa(i+1),
|
||||||
|
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||||
|
GroupID: 1,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = store.EndpointRelation().Create(&portainer.EndpointRelation{
|
||||||
|
EndpointID: portainer.EndpointID(i + 1),
|
||||||
|
EdgeStacks: map[portainer.EdgeStackID]bool{},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(
|
||||||
|
http.MethodPost,
|
||||||
|
"/edge_groups",
|
||||||
|
strings.NewReader(`{"Name": "New Edge Group", "Endpoints": [1, 2, 3]}`),
|
||||||
|
)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
require.Equal(t, http.StatusOK, rr.Result().StatusCode)
|
||||||
|
|
||||||
|
var responseGroup portainer.EdgeGroup
|
||||||
|
err = json.NewDecoder(rr.Body).Decode(&responseGroup)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.ElementsMatch(t, []portainer.EndpointID{1, 2, 3}, responseGroup.Endpoints)
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ import (
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/roar"
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
)
|
)
|
||||||
|
@ -33,7 +34,9 @@ func (handler *Handler) edgeGroupInspect(w http.ResponseWriter, r *http.Request)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
|
|
||||||
return txResponse(w, edgeGroup, err)
|
edgeGroup.Endpoints = edgeGroup.EndpointIDs.ToSlice()
|
||||||
|
|
||||||
|
return txResponse(w, shadowedEdgeGroup{EdgeGroup: *edgeGroup}, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEdgeGroup(tx dataservices.DataStoreTx, ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error) {
|
func getEdgeGroup(tx dataservices.DataStoreTx, ID portainer.EdgeGroupID) (*portainer.EdgeGroup, error) {
|
||||||
|
@ -50,7 +53,7 @@ func getEdgeGroup(tx dataservices.DataStoreTx, ID portainer.EdgeGroupID) (*porta
|
||||||
return nil, httperror.InternalServerError("Unable to retrieve environments and environment groups for Edge group", err)
|
return nil, httperror.InternalServerError("Unable to retrieve environments and environment groups for Edge group", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
edgeGroup.Endpoints = endpoints
|
edgeGroup.EndpointIDs = roar.FromSlice(endpoints)
|
||||||
}
|
}
|
||||||
|
|
||||||
return edgeGroup, err
|
return edgeGroup, err
|
||||||
|
|
137
api/http/handler/edgegroups/edgegroup_inspect_test.go
Normal file
137
api/http/handler/edgegroups/edgegroup_inspect_test.go
Normal file
|
@ -0,0 +1,137 @@
|
||||||
|
package edgegroups
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/datastore"
|
||||||
|
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||||
|
"github.com/portainer/portainer/api/roar"
|
||||||
|
|
||||||
|
"github.com/segmentio/encoding/json"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEdgeGroupInspectHandler(t *testing.T) {
|
||||||
|
_, store := datastore.MustNewTestStore(t, true, true)
|
||||||
|
|
||||||
|
handler := NewHandler(testhelpers.NewTestRequestBouncer())
|
||||||
|
handler.DataStore = store
|
||||||
|
|
||||||
|
err := store.EndpointGroup().Create(&portainer.EndpointGroup{
|
||||||
|
ID: 1,
|
||||||
|
Name: "Test Group",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for i := range 3 {
|
||||||
|
err = store.Endpoint().Create(&portainer.Endpoint{
|
||||||
|
ID: portainer.EndpointID(i + 1),
|
||||||
|
Name: "Test Endpoint " + strconv.Itoa(i+1),
|
||||||
|
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||||
|
GroupID: 1,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = store.EndpointRelation().Create(&portainer.EndpointRelation{
|
||||||
|
EndpointID: portainer.EndpointID(i + 1),
|
||||||
|
EdgeStacks: map[portainer.EdgeStackID]bool{},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.EdgeGroup().Create(&portainer.EdgeGroup{
|
||||||
|
ID: 1,
|
||||||
|
Name: "Test Edge Group",
|
||||||
|
EndpointIDs: roar.FromSlice([]portainer.EndpointID{1, 2, 3}),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(
|
||||||
|
http.MethodGet,
|
||||||
|
"/edge_groups/1",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
require.Equal(t, http.StatusOK, rr.Result().StatusCode)
|
||||||
|
|
||||||
|
var responseGroup portainer.EdgeGroup
|
||||||
|
err = json.NewDecoder(rr.Body).Decode(&responseGroup)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.ElementsMatch(t, []portainer.EndpointID{1, 2, 3}, responseGroup.Endpoints)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDynamicEdgeGroupInspectHandler(t *testing.T) {
|
||||||
|
_, store := datastore.MustNewTestStore(t, true, true)
|
||||||
|
|
||||||
|
handler := NewHandler(testhelpers.NewTestRequestBouncer())
|
||||||
|
handler.DataStore = store
|
||||||
|
|
||||||
|
err := store.EndpointGroup().Create(&portainer.EndpointGroup{
|
||||||
|
ID: 1,
|
||||||
|
Name: "Test Group",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = store.Tag().Create(&portainer.Tag{
|
||||||
|
ID: 1,
|
||||||
|
Name: "Test Tag",
|
||||||
|
Endpoints: map[portainer.EndpointID]bool{
|
||||||
|
1: true,
|
||||||
|
2: true,
|
||||||
|
3: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for i := range 3 {
|
||||||
|
err = store.Endpoint().Create(&portainer.Endpoint{
|
||||||
|
ID: portainer.EndpointID(i + 1),
|
||||||
|
Name: "Test Endpoint " + strconv.Itoa(i+1),
|
||||||
|
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||||
|
GroupID: 1,
|
||||||
|
TagIDs: []portainer.TagID{1},
|
||||||
|
UserTrusted: true,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = store.EndpointRelation().Create(&portainer.EndpointRelation{
|
||||||
|
EndpointID: portainer.EndpointID(i + 1),
|
||||||
|
EdgeStacks: map[portainer.EdgeStackID]bool{},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.EdgeGroup().Create(&portainer.EdgeGroup{
|
||||||
|
ID: 1,
|
||||||
|
Name: "Test Edge Group",
|
||||||
|
Dynamic: true,
|
||||||
|
TagIDs: []portainer.TagID{1},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(
|
||||||
|
http.MethodGet,
|
||||||
|
"/edge_groups/1",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
require.Equal(t, http.StatusOK, rr.Result().StatusCode)
|
||||||
|
|
||||||
|
var responseGroup portainer.EdgeGroup
|
||||||
|
err = json.NewDecoder(rr.Body).Decode(&responseGroup)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.ElementsMatch(t, []portainer.EndpointID{1, 2, 3}, responseGroup.Endpoints)
|
||||||
|
}
|
|
@ -7,11 +7,17 @@ import (
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
|
"github.com/portainer/portainer/api/roar"
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
)
|
)
|
||||||
|
|
||||||
type decoratedEdgeGroup struct {
|
type shadowedEdgeGroup struct {
|
||||||
portainer.EdgeGroup
|
portainer.EdgeGroup
|
||||||
|
EndpointIds int `json:"EndpointIds,omitempty"` // Shadow to avoid exposing in the API
|
||||||
|
}
|
||||||
|
|
||||||
|
type decoratedEdgeGroup struct {
|
||||||
|
shadowedEdgeGroup
|
||||||
HasEdgeStack bool `json:"HasEdgeStack"`
|
HasEdgeStack bool `json:"HasEdgeStack"`
|
||||||
HasEdgeJob bool `json:"HasEdgeJob"`
|
HasEdgeJob bool `json:"HasEdgeJob"`
|
||||||
EndpointTypes []portainer.EndpointType
|
EndpointTypes []portainer.EndpointType
|
||||||
|
@ -76,7 +82,7 @@ func getEdgeGroupList(tx dataservices.DataStoreTx) ([]decoratedEdgeGroup, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
edgeGroup := decoratedEdgeGroup{
|
edgeGroup := decoratedEdgeGroup{
|
||||||
EdgeGroup: orgEdgeGroup,
|
shadowedEdgeGroup: shadowedEdgeGroup{EdgeGroup: orgEdgeGroup},
|
||||||
EndpointTypes: []portainer.EndpointType{},
|
EndpointTypes: []portainer.EndpointType{},
|
||||||
}
|
}
|
||||||
if edgeGroup.Dynamic {
|
if edgeGroup.Dynamic {
|
||||||
|
@ -88,15 +94,16 @@ func getEdgeGroupList(tx dataservices.DataStoreTx) ([]decoratedEdgeGroup, error)
|
||||||
edgeGroup.Endpoints = endpointIDs
|
edgeGroup.Endpoints = endpointIDs
|
||||||
edgeGroup.TrustedEndpoints = endpointIDs
|
edgeGroup.TrustedEndpoints = endpointIDs
|
||||||
} else {
|
} else {
|
||||||
trustedEndpoints, err := getTrustedEndpoints(tx, edgeGroup.Endpoints)
|
trustedEndpoints, err := getTrustedEndpoints(tx, edgeGroup.EndpointIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, httperror.InternalServerError("Unable to retrieve environments for Edge group", err)
|
return nil, httperror.InternalServerError("Unable to retrieve environments for Edge group", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
edgeGroup.Endpoints = edgeGroup.EndpointIDs.ToSlice()
|
||||||
edgeGroup.TrustedEndpoints = trustedEndpoints
|
edgeGroup.TrustedEndpoints = trustedEndpoints
|
||||||
}
|
}
|
||||||
|
|
||||||
endpointTypes, err := getEndpointTypes(tx, edgeGroup.Endpoints)
|
endpointTypes, err := getEndpointTypes(tx, edgeGroup.EndpointIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, httperror.InternalServerError("Unable to retrieve environment types for Edge group", err)
|
return nil, httperror.InternalServerError("Unable to retrieve environment types for Edge group", err)
|
||||||
}
|
}
|
||||||
|
@ -111,15 +118,26 @@ func getEdgeGroupList(tx dataservices.DataStoreTx) ([]decoratedEdgeGroup, error)
|
||||||
return decoratedEdgeGroups, nil
|
return decoratedEdgeGroups, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEndpointTypes(tx dataservices.DataStoreTx, endpointIds []portainer.EndpointID) ([]portainer.EndpointType, error) {
|
func getEndpointTypes(tx dataservices.DataStoreTx, endpointIds roar.Roar[portainer.EndpointID]) ([]portainer.EndpointType, error) {
|
||||||
|
var innerErr error
|
||||||
|
|
||||||
typeSet := map[portainer.EndpointType]bool{}
|
typeSet := map[portainer.EndpointType]bool{}
|
||||||
for _, endpointID := range endpointIds {
|
|
||||||
|
endpointIds.Iterate(func(endpointID portainer.EndpointID) bool {
|
||||||
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
endpoint, err := tx.Endpoint().Endpoint(endpointID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed fetching environment: %w", err)
|
innerErr = fmt.Errorf("failed fetching environment: %w", err)
|
||||||
|
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
typeSet[endpoint.Type] = true
|
typeSet[endpoint.Type] = true
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if innerErr != nil {
|
||||||
|
return nil, innerErr
|
||||||
}
|
}
|
||||||
|
|
||||||
endpointTypes := make([]portainer.EndpointType, 0, len(typeSet))
|
endpointTypes := make([]portainer.EndpointType, 0, len(typeSet))
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
package edgegroups
|
package edgegroups
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/datastore"
|
||||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||||
|
"github.com/portainer/portainer/api/roar"
|
||||||
|
|
||||||
|
"github.com/segmentio/encoding/json"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_getEndpointTypes(t *testing.T) {
|
func Test_getEndpointTypes(t *testing.T) {
|
||||||
|
@ -38,7 +46,7 @@ func Test_getEndpointTypes(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
ans, err := getEndpointTypes(datastore, test.endpointIds)
|
ans, err := getEndpointTypes(datastore, roar.FromSlice(test.endpointIds))
|
||||||
assert.NoError(t, err, "getEndpointTypes shouldn't fail")
|
assert.NoError(t, err, "getEndpointTypes shouldn't fail")
|
||||||
|
|
||||||
assert.ElementsMatch(t, test.expected, ans, "getEndpointTypes expected to return %b for %v, but returned %b", test.expected, test.endpointIds, ans)
|
assert.ElementsMatch(t, test.expected, ans, "getEndpointTypes expected to return %b for %v, but returned %b", test.expected, test.endpointIds, ans)
|
||||||
|
@ -48,6 +56,61 @@ func Test_getEndpointTypes(t *testing.T) {
|
||||||
func Test_getEndpointTypes_failWhenEndpointDontExist(t *testing.T) {
|
func Test_getEndpointTypes_failWhenEndpointDontExist(t *testing.T) {
|
||||||
datastore := testhelpers.NewDatastore(testhelpers.WithEndpoints([]portainer.Endpoint{}))
|
datastore := testhelpers.NewDatastore(testhelpers.WithEndpoints([]portainer.Endpoint{}))
|
||||||
|
|
||||||
_, err := getEndpointTypes(datastore, []portainer.EndpointID{1})
|
_, err := getEndpointTypes(datastore, roar.FromSlice([]portainer.EndpointID{1}))
|
||||||
assert.Error(t, err, "getEndpointTypes should fail")
|
assert.Error(t, err, "getEndpointTypes should fail")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEdgeGroupListHandler(t *testing.T) {
|
||||||
|
_, store := datastore.MustNewTestStore(t, true, true)
|
||||||
|
|
||||||
|
handler := NewHandler(testhelpers.NewTestRequestBouncer())
|
||||||
|
handler.DataStore = store
|
||||||
|
|
||||||
|
err := store.EndpointGroup().Create(&portainer.EndpointGroup{
|
||||||
|
ID: 1,
|
||||||
|
Name: "Test Group",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for i := range 3 {
|
||||||
|
err = store.Endpoint().Create(&portainer.Endpoint{
|
||||||
|
ID: portainer.EndpointID(i + 1),
|
||||||
|
Name: "Test Endpoint " + strconv.Itoa(i+1),
|
||||||
|
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||||
|
GroupID: 1,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = store.EndpointRelation().Create(&portainer.EndpointRelation{
|
||||||
|
EndpointID: portainer.EndpointID(i + 1),
|
||||||
|
EdgeStacks: map[portainer.EdgeStackID]bool{},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.EdgeGroup().Create(&portainer.EdgeGroup{
|
||||||
|
ID: 1,
|
||||||
|
Name: "Test Edge Group",
|
||||||
|
EndpointIDs: roar.FromSlice([]portainer.EndpointID{1, 2, 3}),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(
|
||||||
|
http.MethodGet,
|
||||||
|
"/edge_groups",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
require.Equal(t, http.StatusOK, rr.Result().StatusCode)
|
||||||
|
|
||||||
|
var responseGroups []decoratedEdgeGroup
|
||||||
|
err = json.NewDecoder(rr.Body).Decode(&responseGroups)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, responseGroups, 1)
|
||||||
|
require.ElementsMatch(t, []portainer.EndpointID{1, 2, 3}, responseGroups[0].Endpoints)
|
||||||
|
require.Len(t, responseGroups[0].TrustedEndpoints, 0)
|
||||||
|
}
|
||||||
|
|
|
@ -158,7 +158,7 @@ func (handler *Handler) edgeGroupUpdate(w http.ResponseWriter, r *http.Request)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
return txResponse(w, edgeGroup, err)
|
return txResponse(w, shadowedEdgeGroup{EdgeGroup: *edgeGroup}, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) updateEndpointStacks(tx dataservices.DataStoreTx, endpoint *portainer.Endpoint, edgeGroups []portainer.EdgeGroup, edgeStacks []portainer.EdgeStack) error {
|
func (handler *Handler) updateEndpointStacks(tx dataservices.DataStoreTx, endpoint *portainer.Endpoint, edgeGroups []portainer.EdgeGroup, edgeStacks []portainer.EdgeStack) error {
|
||||||
|
|
70
api/http/handler/edgegroups/edgegroup_update_test.go
Normal file
70
api/http/handler/edgegroups/edgegroup_update_test.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package edgegroups
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/datastore"
|
||||||
|
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||||
|
"github.com/portainer/portainer/api/roar"
|
||||||
|
|
||||||
|
"github.com/segmentio/encoding/json"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEdgeGroupUpdateHandler(t *testing.T) {
|
||||||
|
_, store := datastore.MustNewTestStore(t, true, true)
|
||||||
|
|
||||||
|
handler := NewHandler(testhelpers.NewTestRequestBouncer())
|
||||||
|
handler.DataStore = store
|
||||||
|
|
||||||
|
err := store.EndpointGroup().Create(&portainer.EndpointGroup{
|
||||||
|
ID: 1,
|
||||||
|
Name: "Test Group",
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
for i := range 3 {
|
||||||
|
err = store.Endpoint().Create(&portainer.Endpoint{
|
||||||
|
ID: portainer.EndpointID(i + 1),
|
||||||
|
Name: "Test Endpoint " + strconv.Itoa(i+1),
|
||||||
|
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||||
|
GroupID: 1,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = store.EndpointRelation().Create(&portainer.EndpointRelation{
|
||||||
|
EndpointID: portainer.EndpointID(i + 1),
|
||||||
|
EdgeStacks: map[portainer.EdgeStackID]bool{},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = store.EdgeGroup().Create(&portainer.EdgeGroup{
|
||||||
|
ID: 1,
|
||||||
|
Name: "Test Edge Group",
|
||||||
|
EndpointIDs: roar.FromSlice([]portainer.EndpointID{1}),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(
|
||||||
|
http.MethodPut,
|
||||||
|
"/edge_groups/1",
|
||||||
|
strings.NewReader(`{"Endpoints": [1, 2, 3]}`),
|
||||||
|
)
|
||||||
|
|
||||||
|
handler.ServeHTTP(rr, req)
|
||||||
|
require.Equal(t, http.StatusOK, rr.Result().StatusCode)
|
||||||
|
|
||||||
|
var responseGroup portainer.EdgeGroup
|
||||||
|
err = json.NewDecoder(rr.Body).Decode(&responseGroup)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.ElementsMatch(t, []portainer.EndpointID{1, 2, 3}, responseGroup.Endpoints)
|
||||||
|
}
|
|
@ -8,9 +8,10 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/portainer/portainer/api/roar"
|
||||||
|
|
||||||
"github.com/segmentio/encoding/json"
|
"github.com/segmentio/encoding/json"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Create
|
// Create
|
||||||
|
@ -24,7 +25,7 @@ func TestCreateAndInspect(t *testing.T) {
|
||||||
Name: "EdgeGroup 1",
|
Name: "EdgeGroup 1",
|
||||||
Dynamic: false,
|
Dynamic: false,
|
||||||
TagIDs: nil,
|
TagIDs: nil,
|
||||||
Endpoints: []portainer.EndpointID{endpoint.ID},
|
EndpointIDs: roar.FromSlice([]portainer.EndpointID{endpoint.ID}),
|
||||||
PartialMatch: false,
|
PartialMatch: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"github.com/portainer/portainer/api/internal/edge/edgestacks"
|
"github.com/portainer/portainer/api/internal/edge/edgestacks"
|
||||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||||
"github.com/portainer/portainer/api/jwt"
|
"github.com/portainer/portainer/api/jwt"
|
||||||
|
"github.com/portainer/portainer/api/roar"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -103,7 +104,7 @@ func createEdgeStack(t *testing.T, store dataservices.DataStore, endpointID port
|
||||||
Name: "EdgeGroup 1",
|
Name: "EdgeGroup 1",
|
||||||
Dynamic: false,
|
Dynamic: false,
|
||||||
TagIDs: nil,
|
TagIDs: nil,
|
||||||
Endpoints: []portainer.EndpointID{endpointID},
|
EndpointIDs: roar.FromSlice([]portainer.EndpointID{endpointID}),
|
||||||
PartialMatch: false,
|
PartialMatch: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,10 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/portainer/portainer/api/roar"
|
||||||
|
|
||||||
"github.com/segmentio/encoding/json"
|
"github.com/segmentio/encoding/json"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Update
|
// Update
|
||||||
|
@ -43,7 +44,7 @@ func TestUpdateAndInspect(t *testing.T) {
|
||||||
Name: "EdgeGroup 2",
|
Name: "EdgeGroup 2",
|
||||||
Dynamic: false,
|
Dynamic: false,
|
||||||
TagIDs: nil,
|
TagIDs: nil,
|
||||||
Endpoints: []portainer.EndpointID{newEndpoint.ID},
|
EndpointIDs: roar.FromSlice([]portainer.EndpointID{newEndpoint.ID}),
|
||||||
PartialMatch: false,
|
PartialMatch: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,7 +113,7 @@ func TestUpdateWithInvalidEdgeGroups(t *testing.T) {
|
||||||
Name: "EdgeGroup 2",
|
Name: "EdgeGroup 2",
|
||||||
Dynamic: false,
|
Dynamic: false,
|
||||||
TagIDs: nil,
|
TagIDs: nil,
|
||||||
Endpoints: []portainer.EndpointID{8889},
|
EndpointIDs: roar.FromSlice([]portainer.EndpointID{8889}),
|
||||||
PartialMatch: false,
|
PartialMatch: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/jwt"
|
"github.com/portainer/portainer/api/jwt"
|
||||||
|
"github.com/portainer/portainer/api/roar"
|
||||||
|
|
||||||
"github.com/segmentio/encoding/json"
|
"github.com/segmentio/encoding/json"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
@ -367,7 +368,7 @@ func TestEdgeJobsResponse(t *testing.T) {
|
||||||
|
|
||||||
staticEdgeGroup := portainer.EdgeGroup{
|
staticEdgeGroup := portainer.EdgeGroup{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Endpoints: []portainer.EndpointID{endpointFromStaticEdgeGroup.ID},
|
EndpointIDs: roar.FromSlice([]portainer.EndpointID{endpointFromStaticEdgeGroup.ID}),
|
||||||
}
|
}
|
||||||
err := handler.DataStore.EdgeGroup().Create(&staticEdgeGroup)
|
err := handler.DataStore.EdgeGroup().Create(&staticEdgeGroup)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
|
@ -3,7 +3,6 @@ package endpoints
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
@ -200,9 +199,7 @@ func (handler *Handler) deleteEndpoint(tx dataservices.DataStoreTx, endpointID p
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, edgeGroup := range edgeGroups {
|
for _, edgeGroup := range edgeGroups {
|
||||||
edgeGroup.Endpoints = slices.DeleteFunc(edgeGroup.Endpoints, func(e portainer.EndpointID) bool {
|
edgeGroup.EndpointIDs.Remove(endpoint.ID)
|
||||||
return e == endpoint.ID
|
|
||||||
})
|
|
||||||
|
|
||||||
if err := tx.EdgeGroup().Update(edgeGroup.ID, &edgeGroup); err != nil {
|
if err := tx.EdgeGroup().Update(edgeGroup.ID, &edgeGroup); err != nil {
|
||||||
log.Warn().Err(err).Msg("Unable to update edge group")
|
log.Warn().Err(err).Msg("Unable to update edge group")
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"github.com/portainer/portainer/api/datastore"
|
"github.com/portainer/portainer/api/datastore"
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
"github.com/portainer/portainer/api/http/proxy"
|
||||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||||
|
"github.com/portainer/portainer/api/roar"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestEndpointDeleteEdgeGroupsConcurrently(t *testing.T) {
|
func TestEndpointDeleteEdgeGroupsConcurrently(t *testing.T) {
|
||||||
|
@ -44,7 +45,7 @@ func TestEndpointDeleteEdgeGroupsConcurrently(t *testing.T) {
|
||||||
if err := store.EdgeGroup().Create(&portainer.EdgeGroup{
|
if err := store.EdgeGroup().Create(&portainer.EdgeGroup{
|
||||||
ID: 1,
|
ID: 1,
|
||||||
Name: "edgegroup-1",
|
Name: "edgegroup-1",
|
||||||
Endpoints: endpointIDs,
|
EndpointIDs: roar.FromSlice(endpointIDs),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
t.Fatal("could not create edge group:", err)
|
t.Fatal("could not create edge group:", err)
|
||||||
}
|
}
|
||||||
|
@ -78,7 +79,7 @@ func TestEndpointDeleteEdgeGroupsConcurrently(t *testing.T) {
|
||||||
t.Fatal("could not retrieve the edge group:", err)
|
t.Fatal("could not retrieve the edge group:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(edgeGroup.Endpoints) > 0 {
|
if edgeGroup.EndpointIDs.Len() > 0 {
|
||||||
t.Fatal("the edge group is not consistent")
|
t.Fatal("the edge group is not consistent")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ import (
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/internal/edge"
|
"github.com/portainer/portainer/api/internal/edge"
|
||||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||||
"github.com/portainer/portainer/api/slicesx"
|
"github.com/portainer/portainer/api/roar"
|
||||||
"github.com/portainer/portainer/pkg/libhttp/request"
|
"github.com/portainer/portainer/pkg/libhttp/request"
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
@ -146,7 +146,9 @@ func (handler *Handler) filterEndpointsByQuery(
|
||||||
totalAvailableEndpoints := len(filteredEndpoints)
|
totalAvailableEndpoints := len(filteredEndpoints)
|
||||||
|
|
||||||
if len(query.endpointIds) > 0 {
|
if len(query.endpointIds) > 0 {
|
||||||
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, query.endpointIds)
|
endpointIDs := roar.FromSlice(query.endpointIds)
|
||||||
|
|
||||||
|
filteredEndpoints = filteredEndpointsByIds(filteredEndpoints, endpointIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(query.excludeIds) > 0 {
|
if len(query.excludeIds) > 0 {
|
||||||
|
@ -275,7 +277,7 @@ func filterEndpointsByEdgeStack(endpoints []portainer.Endpoint, edgeStackId port
|
||||||
return nil, errors.WithMessage(err, "Unable to retrieve edge stack from the database")
|
return nil, errors.WithMessage(err, "Unable to retrieve edge stack from the database")
|
||||||
}
|
}
|
||||||
|
|
||||||
envIds := make([]portainer.EndpointID, 0)
|
envIds := roar.Roar[portainer.EndpointID]{}
|
||||||
for _, edgeGroupdId := range stack.EdgeGroups {
|
for _, edgeGroupdId := range stack.EdgeGroups {
|
||||||
edgeGroup, err := datastore.EdgeGroup().Read(edgeGroupdId)
|
edgeGroup, err := datastore.EdgeGroup().Read(edgeGroupdId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -287,32 +289,37 @@ func filterEndpointsByEdgeStack(endpoints []portainer.Endpoint, edgeStackId port
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.WithMessage(err, "Unable to retrieve environments and environment groups for Edge group")
|
return nil, errors.WithMessage(err, "Unable to retrieve environments and environment groups for Edge group")
|
||||||
}
|
}
|
||||||
edgeGroup.Endpoints = endpointIDs
|
edgeGroup.EndpointIDs = roar.FromSlice(endpointIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
envIds = append(envIds, edgeGroup.Endpoints...)
|
envIds.Union(edgeGroup.EndpointIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
if statusFilter != nil {
|
if statusFilter != nil {
|
||||||
n := 0
|
var innerErr error
|
||||||
for _, envId := range envIds {
|
|
||||||
|
envIds.Iterate(func(envId portainer.EndpointID) bool {
|
||||||
edgeStackStatus, err := datastore.EdgeStackStatus().Read(edgeStackId, envId)
|
edgeStackStatus, err := datastore.EdgeStackStatus().Read(edgeStackId, envId)
|
||||||
if dataservices.IsErrObjectNotFound(err) {
|
if dataservices.IsErrObjectNotFound(err) {
|
||||||
continue
|
return true
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return nil, errors.WithMessagef(err, "Unable to retrieve edge stack status for environment %d", envId)
|
innerErr = errors.WithMessagef(err, "Unable to retrieve edge stack status for environment %d", envId)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if endpointStatusInStackMatchesFilter(edgeStackStatus, envId, *statusFilter) {
|
if !endpointStatusInStackMatchesFilter(edgeStackStatus, portainer.EndpointID(envId), *statusFilter) {
|
||||||
envIds[n] = envId
|
envIds.Remove(envId)
|
||||||
n++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
envIds = envIds[:n]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uniqueIds := slicesx.Unique(envIds)
|
return true
|
||||||
filteredEndpoints := filteredEndpointsByIds(endpoints, uniqueIds)
|
})
|
||||||
|
|
||||||
|
if innerErr != nil {
|
||||||
|
return nil, innerErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredEndpoints := filteredEndpointsByIds(endpoints, envIds)
|
||||||
|
|
||||||
return filteredEndpoints, nil
|
return filteredEndpoints, nil
|
||||||
}
|
}
|
||||||
|
@ -344,16 +351,14 @@ func filterEndpointsByEdgeGroupIDs(endpoints []portainer.Endpoint, edgeGroups []
|
||||||
}
|
}
|
||||||
edgeGroups = edgeGroups[:n]
|
edgeGroups = edgeGroups[:n]
|
||||||
|
|
||||||
endpointIDSet := make(map[portainer.EndpointID]struct{})
|
endpointIDSet := roar.Roar[portainer.EndpointID]{}
|
||||||
for _, edgeGroup := range edgeGroups {
|
for _, edgeGroup := range edgeGroups {
|
||||||
for _, endpointID := range edgeGroup.Endpoints {
|
endpointIDSet.Union(edgeGroup.EndpointIDs)
|
||||||
endpointIDSet[endpointID] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
n = 0
|
n = 0
|
||||||
for _, endpoint := range endpoints {
|
for _, endpoint := range endpoints {
|
||||||
if _, exists := endpointIDSet[endpoint.ID]; exists {
|
if endpointIDSet.Contains(endpoint.ID) {
|
||||||
endpoints[n] = endpoint
|
endpoints[n] = endpoint
|
||||||
n++
|
n++
|
||||||
}
|
}
|
||||||
|
@ -369,12 +374,11 @@ func filterEndpointsByExcludeEdgeGroupIDs(endpoints []portainer.Endpoint, edgeGr
|
||||||
}
|
}
|
||||||
|
|
||||||
n := 0
|
n := 0
|
||||||
excludeEndpointIDSet := make(map[portainer.EndpointID]struct{})
|
excludeEndpointIDSet := roar.Roar[portainer.EndpointID]{}
|
||||||
|
|
||||||
for _, edgeGroup := range edgeGroups {
|
for _, edgeGroup := range edgeGroups {
|
||||||
if _, ok := excludeEdgeGroupIDSet[edgeGroup.ID]; ok {
|
if _, ok := excludeEdgeGroupIDSet[edgeGroup.ID]; ok {
|
||||||
for _, endpointID := range edgeGroup.Endpoints {
|
excludeEndpointIDSet.Union(edgeGroup.EndpointIDs)
|
||||||
excludeEndpointIDSet[endpointID] = struct{}{}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
edgeGroups[n] = edgeGroup
|
edgeGroups[n] = edgeGroup
|
||||||
n++
|
n++
|
||||||
|
@ -384,7 +388,7 @@ func filterEndpointsByExcludeEdgeGroupIDs(endpoints []portainer.Endpoint, edgeGr
|
||||||
|
|
||||||
n = 0
|
n = 0
|
||||||
for _, endpoint := range endpoints {
|
for _, endpoint := range endpoints {
|
||||||
if _, ok := excludeEndpointIDSet[endpoint.ID]; !ok {
|
if !excludeEndpointIDSet.Contains(endpoint.ID) {
|
||||||
endpoints[n] = endpoint
|
endpoints[n] = endpoint
|
||||||
n++
|
n++
|
||||||
}
|
}
|
||||||
|
@ -609,15 +613,10 @@ func endpointFullMatchTags(endpoint portainer.Endpoint, endpointGroup portainer.
|
||||||
return len(missingTags) == 0
|
return len(missingTags) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func filteredEndpointsByIds(endpoints []portainer.Endpoint, ids []portainer.EndpointID) []portainer.Endpoint {
|
func filteredEndpointsByIds(endpoints []portainer.Endpoint, ids roar.Roar[portainer.EndpointID]) []portainer.Endpoint {
|
||||||
idsSet := make(map[portainer.EndpointID]bool, len(ids))
|
|
||||||
for _, id := range ids {
|
|
||||||
idsSet[id] = true
|
|
||||||
}
|
|
||||||
|
|
||||||
n := 0
|
n := 0
|
||||||
for _, endpoint := range endpoints {
|
for _, endpoint := range endpoints {
|
||||||
if idsSet[endpoint.ID] {
|
if ids.Contains(endpoint.ID) {
|
||||||
endpoints[n] = endpoint
|
endpoints[n] = endpoint
|
||||||
n++
|
n++
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,9 +8,11 @@ import (
|
||||||
"github.com/portainer/portainer/api/datastore"
|
"github.com/portainer/portainer/api/datastore"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||||
|
"github.com/portainer/portainer/api/roar"
|
||||||
"github.com/portainer/portainer/api/slicesx"
|
"github.com/portainer/portainer/api/slicesx"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
type filterTest struct {
|
type filterTest struct {
|
||||||
|
@ -175,7 +177,7 @@ func BenchmarkFilterEndpointsBySearchCriteria_PartialMatch(b *testing.B) {
|
||||||
edgeGroups = append(edgeGroups, portainer.EdgeGroup{
|
edgeGroups = append(edgeGroups, portainer.EdgeGroup{
|
||||||
ID: portainer.EdgeGroupID(i + 1),
|
ID: portainer.EdgeGroupID(i + 1),
|
||||||
Name: "edge-group-" + strconv.Itoa(i+1),
|
Name: "edge-group-" + strconv.Itoa(i+1),
|
||||||
Endpoints: append([]portainer.EndpointID{}, endpointIDs...),
|
EndpointIDs: roar.FromSlice(endpointIDs),
|
||||||
Dynamic: true,
|
Dynamic: true,
|
||||||
TagIDs: []portainer.TagID{1, 2, 3},
|
TagIDs: []portainer.TagID{1, 2, 3},
|
||||||
PartialMatch: true,
|
PartialMatch: true,
|
||||||
|
@ -224,7 +226,7 @@ func BenchmarkFilterEndpointsBySearchCriteria_FullMatch(b *testing.B) {
|
||||||
edgeGroups = append(edgeGroups, portainer.EdgeGroup{
|
edgeGroups = append(edgeGroups, portainer.EdgeGroup{
|
||||||
ID: portainer.EdgeGroupID(i + 1),
|
ID: portainer.EdgeGroupID(i + 1),
|
||||||
Name: "edge-group-" + strconv.Itoa(i+1),
|
Name: "edge-group-" + strconv.Itoa(i+1),
|
||||||
Endpoints: append([]portainer.EndpointID{}, endpointIDs...),
|
EndpointIDs: roar.FromSlice(endpointIDs),
|
||||||
Dynamic: true,
|
Dynamic: true,
|
||||||
TagIDs: []portainer.TagID{1},
|
TagIDs: []portainer.TagID{1},
|
||||||
})
|
})
|
||||||
|
@ -300,3 +302,127 @@ func setupFilterTest(t *testing.T, endpoints []portainer.Endpoint) *Handler {
|
||||||
|
|
||||||
return handler
|
return handler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFilterEndpointsByEdgeStack(t *testing.T) {
|
||||||
|
_, store := datastore.MustNewTestStore(t, false, false)
|
||||||
|
|
||||||
|
endpoints := []portainer.Endpoint{
|
||||||
|
{ID: 1, Name: "Endpoint 1"},
|
||||||
|
{ID: 2, Name: "Endpoint 2"},
|
||||||
|
{ID: 3, Name: "Endpoint 3"},
|
||||||
|
{ID: 4, Name: "Endpoint 4"},
|
||||||
|
}
|
||||||
|
|
||||||
|
edgeStackId := portainer.EdgeStackID(1)
|
||||||
|
|
||||||
|
err := store.EdgeStack().Create(edgeStackId, &portainer.EdgeStack{
|
||||||
|
ID: edgeStackId,
|
||||||
|
Name: "Test Edge Stack",
|
||||||
|
EdgeGroups: []portainer.EdgeGroupID{1, 2},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = store.EdgeGroup().Create(&portainer.EdgeGroup{
|
||||||
|
ID: 1,
|
||||||
|
Name: "Edge Group 1",
|
||||||
|
EndpointIDs: roar.FromSlice([]portainer.EndpointID{1}),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = store.EdgeGroup().Create(&portainer.EdgeGroup{
|
||||||
|
ID: 2,
|
||||||
|
Name: "Edge Group 2",
|
||||||
|
EndpointIDs: roar.FromSlice([]portainer.EndpointID{2, 3}),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
es, err := filterEndpointsByEdgeStack(endpoints, edgeStackId, nil, store)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, es, 3)
|
||||||
|
require.Contains(t, es, endpoints[0]) // Endpoint 1
|
||||||
|
require.Contains(t, es, endpoints[1]) // Endpoint 2
|
||||||
|
require.Contains(t, es, endpoints[2]) // Endpoint 3
|
||||||
|
require.NotContains(t, es, endpoints[3]) // Endpoint 4
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterEndpointsByEdgeGroup(t *testing.T) {
|
||||||
|
_, store := datastore.MustNewTestStore(t, false, false)
|
||||||
|
|
||||||
|
endpoints := []portainer.Endpoint{
|
||||||
|
{ID: 1, Name: "Endpoint 1"},
|
||||||
|
{ID: 2, Name: "Endpoint 2"},
|
||||||
|
{ID: 3, Name: "Endpoint 3"},
|
||||||
|
{ID: 4, Name: "Endpoint 4"},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := store.EdgeGroup().Create(&portainer.EdgeGroup{
|
||||||
|
ID: 1,
|
||||||
|
Name: "Edge Group 1",
|
||||||
|
EndpointIDs: roar.FromSlice([]portainer.EndpointID{1}),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = store.EdgeGroup().Create(&portainer.EdgeGroup{
|
||||||
|
ID: 2,
|
||||||
|
Name: "Edge Group 2",
|
||||||
|
EndpointIDs: roar.FromSlice([]portainer.EndpointID{2, 3}),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
edgeGroups, err := store.EdgeGroup().ReadAll()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
es, egs := filterEndpointsByEdgeGroupIDs(endpoints, edgeGroups, []portainer.EdgeGroupID{1, 2})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, es, 3)
|
||||||
|
require.Contains(t, es, endpoints[0]) // Endpoint 1
|
||||||
|
require.Contains(t, es, endpoints[1]) // Endpoint 2
|
||||||
|
require.Contains(t, es, endpoints[2]) // Endpoint 3
|
||||||
|
require.NotContains(t, es, endpoints[3]) // Endpoint 4
|
||||||
|
|
||||||
|
require.Len(t, egs, 2)
|
||||||
|
require.Equal(t, egs[0].ID, portainer.EdgeGroupID(1))
|
||||||
|
require.Equal(t, egs[1].ID, portainer.EdgeGroupID(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFilterEndpointsByExcludeEdgeGroupIDs(t *testing.T) {
|
||||||
|
_, store := datastore.MustNewTestStore(t, false, false)
|
||||||
|
|
||||||
|
endpoints := []portainer.Endpoint{
|
||||||
|
{ID: 1, Name: "Endpoint 1"},
|
||||||
|
{ID: 2, Name: "Endpoint 2"},
|
||||||
|
{ID: 3, Name: "Endpoint 3"},
|
||||||
|
{ID: 4, Name: "Endpoint 4"},
|
||||||
|
}
|
||||||
|
|
||||||
|
err := store.EdgeGroup().Create(&portainer.EdgeGroup{
|
||||||
|
ID: 1,
|
||||||
|
Name: "Edge Group 1",
|
||||||
|
EndpointIDs: roar.FromSlice([]portainer.EndpointID{1}),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = store.EdgeGroup().Create(&portainer.EdgeGroup{
|
||||||
|
ID: 2,
|
||||||
|
Name: "Edge Group 2",
|
||||||
|
EndpointIDs: roar.FromSlice([]portainer.EndpointID{2, 3}),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
edgeGroups, err := store.EdgeGroup().ReadAll()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
es, egs := filterEndpointsByExcludeEdgeGroupIDs(endpoints, edgeGroups, []portainer.EdgeGroupID{1})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Len(t, es, 3)
|
||||||
|
require.Equal(t, es, []portainer.Endpoint{
|
||||||
|
{ID: 2, Name: "Endpoint 2"},
|
||||||
|
{ID: 3, Name: "Endpoint 3"},
|
||||||
|
{ID: 4, Name: "Endpoint 4"},
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Len(t, egs, 1)
|
||||||
|
require.Equal(t, egs[0].ID, portainer.EdgeGroupID(2))
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
package endpoints
|
package endpoints
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"slices"
|
|
||||||
|
|
||||||
"github.com/pkg/errors"
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/portainer/portainer/api/set"
|
"github.com/portainer/portainer/api/set"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func updateEnvironmentEdgeGroups(tx dataservices.DataStoreTx, newEdgeGroups []portainer.EdgeGroupID, environmentID portainer.EndpointID) (bool, error) {
|
func updateEnvironmentEdgeGroups(tx dataservices.DataStoreTx, newEdgeGroups []portainer.EdgeGroupID, environmentID portainer.EndpointID) (bool, error) {
|
||||||
|
@ -19,12 +18,10 @@ func updateEnvironmentEdgeGroups(tx dataservices.DataStoreTx, newEdgeGroups []po
|
||||||
|
|
||||||
environmentEdgeGroupsSet := set.Set[portainer.EdgeGroupID]{}
|
environmentEdgeGroupsSet := set.Set[portainer.EdgeGroupID]{}
|
||||||
for _, edgeGroup := range edgeGroups {
|
for _, edgeGroup := range edgeGroups {
|
||||||
for _, eID := range edgeGroup.Endpoints {
|
if edgeGroup.EndpointIDs.Contains(environmentID) {
|
||||||
if eID == environmentID {
|
|
||||||
environmentEdgeGroupsSet[edgeGroup.ID] = true
|
environmentEdgeGroupsSet[edgeGroup.ID] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
union := set.Union(newEdgeGroupsSet, environmentEdgeGroupsSet)
|
union := set.Union(newEdgeGroupsSet, environmentEdgeGroupsSet)
|
||||||
intersection := set.Intersection(newEdgeGroupsSet, environmentEdgeGroupsSet)
|
intersection := set.Intersection(newEdgeGroupsSet, environmentEdgeGroupsSet)
|
||||||
|
@ -52,20 +49,16 @@ func updateEnvironmentEdgeGroups(tx dataservices.DataStoreTx, newEdgeGroups []po
|
||||||
}
|
}
|
||||||
|
|
||||||
removeEdgeGroups := environmentEdgeGroupsSet.Difference(newEdgeGroupsSet)
|
removeEdgeGroups := environmentEdgeGroupsSet.Difference(newEdgeGroupsSet)
|
||||||
err = updateSet(removeEdgeGroups, func(edgeGroup *portainer.EdgeGroup) {
|
if err := updateSet(removeEdgeGroups, func(edgeGroup *portainer.EdgeGroup) {
|
||||||
edgeGroup.Endpoints = slices.DeleteFunc(edgeGroup.Endpoints, func(eID portainer.EndpointID) bool {
|
edgeGroup.EndpointIDs.Remove(environmentID)
|
||||||
return eID == environmentID
|
}); err != nil {
|
||||||
})
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
addToEdgeGroups := newEdgeGroupsSet.Difference(environmentEdgeGroupsSet)
|
addToEdgeGroups := newEdgeGroupsSet.Difference(environmentEdgeGroupsSet)
|
||||||
err = updateSet(addToEdgeGroups, func(edgeGroup *portainer.EdgeGroup) {
|
if err := updateSet(addToEdgeGroups, func(edgeGroup *portainer.EdgeGroup) {
|
||||||
edgeGroup.Endpoints = append(edgeGroup.Endpoints, environmentID)
|
edgeGroup.EndpointIDs.Add(environmentID)
|
||||||
})
|
}); err != nil {
|
||||||
if err != nil {
|
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/portainer/portainer/api/datastore"
|
"github.com/portainer/portainer/api/datastore"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -17,7 +18,6 @@ func Test_updateEdgeGroups(t *testing.T) {
|
||||||
Name: name,
|
Name: name,
|
||||||
Dynamic: false,
|
Dynamic: false,
|
||||||
TagIDs: make([]portainer.TagID, 0),
|
TagIDs: make([]portainer.TagID, 0),
|
||||||
Endpoints: make([]portainer.EndpointID, 0),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := store.EdgeGroup().Create(group); err != nil {
|
if err := store.EdgeGroup().Create(group); err != nil {
|
||||||
|
@ -35,13 +35,8 @@ func Test_updateEdgeGroups(t *testing.T) {
|
||||||
group, err := store.EdgeGroup().Read(groupID)
|
group, err := store.EdgeGroup().Read(groupID)
|
||||||
is.NoError(err)
|
is.NoError(err)
|
||||||
|
|
||||||
for _, endpoint := range group.Endpoints {
|
is.True(group.EndpointIDs.Contains(endpointID),
|
||||||
if endpoint == endpointID {
|
"expected endpoint to be in group")
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
is.Fail("expected endpoint to be in group")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +76,7 @@ func Test_updateEdgeGroups(t *testing.T) {
|
||||||
|
|
||||||
endpointGroups := groupsByName(groups, testCase.endpointGroupNames)
|
endpointGroups := groupsByName(groups, testCase.endpointGroupNames)
|
||||||
for _, group := range endpointGroups {
|
for _, group := range endpointGroups {
|
||||||
group.Endpoints = append(group.Endpoints, testCase.endpoint.ID)
|
group.EndpointIDs.Add(testCase.endpoint.ID)
|
||||||
|
|
||||||
err = store.EdgeGroup().Update(group.ID, &group)
|
err = store.EdgeGroup().Update(group.ID, &group)
|
||||||
is.NoError(err)
|
is.NoError(err)
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_updateTags(t *testing.T) {
|
func Test_updateTags(t *testing.T) {
|
||||||
|
|
||||||
createTags := func(store *datastore.Store, tagNames []string) ([]portainer.Tag, error) {
|
createTags := func(store *datastore.Store, tagNames []string) ([]portainer.Tag, error) {
|
||||||
tags := make([]portainer.Tag, len(tagNames))
|
tags := make([]portainer.Tag, len(tagNames))
|
||||||
for index, tagName := range tagNames {
|
for index, tagName := range tagNames {
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package tags
|
package tags
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -9,9 +8,11 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
portainerDsErrors "github.com/portainer/portainer/api/dataservices/errors"
|
portainerDsErrors "github.com/portainer/portainer/api/dataservices/errors"
|
||||||
"github.com/portainer/portainer/api/datastore"
|
"github.com/portainer/portainer/api/datastore"
|
||||||
"github.com/portainer/portainer/api/internal/testhelpers"
|
"github.com/portainer/portainer/api/internal/testhelpers"
|
||||||
|
"github.com/portainer/portainer/api/roar"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
@ -129,7 +130,7 @@ func TestHandler_tagDelete(t *testing.T) {
|
||||||
staticEdgeGroup := &portainer.EdgeGroup{
|
staticEdgeGroup := &portainer.EdgeGroup{
|
||||||
ID: 2,
|
ID: 2,
|
||||||
Name: "edgegroup-2",
|
Name: "edgegroup-2",
|
||||||
Endpoints: []portainer.EndpointID{endpoint2.ID},
|
EndpointIDs: roar.FromSlice([]portainer.EndpointID{endpoint2.ID}),
|
||||||
}
|
}
|
||||||
require.NoError(t, store.EdgeGroup().Create(staticEdgeGroup))
|
require.NoError(t, store.EdgeGroup().Create(staticEdgeGroup))
|
||||||
|
|
||||||
|
@ -163,14 +164,14 @@ func TestHandler_tagDelete(t *testing.T) {
|
||||||
dynamicEdgeGroup, err = store.EdgeGroup().Read(dynamicEdgeGroup.ID)
|
dynamicEdgeGroup, err = store.EdgeGroup().Read(dynamicEdgeGroup.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, dynamicEdgeGroup.TagIDs, 0, "dynamic edge group should not have any tags")
|
assert.Len(t, dynamicEdgeGroup.TagIDs, 0, "dynamic edge group should not have any tags")
|
||||||
assert.Len(t, dynamicEdgeGroup.Endpoints, 0, "dynamic edge group should not have any endpoints")
|
assert.Equal(t, 0, dynamicEdgeGroup.EndpointIDs.Len(), "dynamic edge group should not have any endpoints")
|
||||||
|
|
||||||
// Check that the static edge group is not updated
|
// Check that the static edge group is not updated
|
||||||
staticEdgeGroup, err = store.EdgeGroup().Read(staticEdgeGroup.ID)
|
staticEdgeGroup, err = store.EdgeGroup().Read(staticEdgeGroup.ID)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, staticEdgeGroup.TagIDs, 0, "static edge group should not have any tags")
|
assert.Len(t, staticEdgeGroup.TagIDs, 0, "static edge group should not have any tags")
|
||||||
assert.Len(t, staticEdgeGroup.Endpoints, 1, "static edge group should have one endpoint")
|
assert.Equal(t, 1, staticEdgeGroup.EndpointIDs.Len(), "static edge group should have one endpoint")
|
||||||
assert.Equal(t, endpoint2.ID, staticEdgeGroup.Endpoints[0], "static edge group should have the endpoint-2")
|
assert.True(t, staticEdgeGroup.EndpointIDs.Contains(endpoint2.ID), "static edge group should have the endpoint-2")
|
||||||
})
|
})
|
||||||
|
|
||||||
// Test the tx.IsErrObjectNotFound logic when endpoint is not found during cleanup
|
// Test the tx.IsErrObjectNotFound logic when endpoint is not found during cleanup
|
||||||
|
@ -185,14 +186,10 @@ func TestHandler_tagDelete(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
err := store.Tag().Create(tag)
|
err := store.Tag().Create(tag)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatal("could not create tag:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = deleteTag(store, 1)
|
err = deleteTag(store, 1)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatal("could not delete tag:", err)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package edge
|
package edge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"slices"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
portainer "github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/dataservices"
|
"github.com/portainer/portainer/api/dataservices"
|
||||||
"github.com/portainer/portainer/api/internal/endpointutils"
|
"github.com/portainer/portainer/api/internal/endpointutils"
|
||||||
|
@ -12,7 +10,7 @@ import (
|
||||||
// EdgeGroupRelatedEndpoints returns a list of environments(endpoints) related to this Edge group
|
// EdgeGroupRelatedEndpoints returns a list of environments(endpoints) related to this Edge group
|
||||||
func EdgeGroupRelatedEndpoints(edgeGroup *portainer.EdgeGroup, endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup) []portainer.EndpointID {
|
func EdgeGroupRelatedEndpoints(edgeGroup *portainer.EdgeGroup, endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup) []portainer.EndpointID {
|
||||||
if !edgeGroup.Dynamic {
|
if !edgeGroup.Dynamic {
|
||||||
return edgeGroup.Endpoints
|
return edgeGroup.EndpointIDs.ToSlice()
|
||||||
}
|
}
|
||||||
|
|
||||||
endpointGroupsMap := map[portainer.EndpointGroupID]*portainer.EndpointGroup{}
|
endpointGroupsMap := map[portainer.EndpointGroupID]*portainer.EndpointGroup{}
|
||||||
|
@ -72,7 +70,7 @@ func GetEndpointsFromEdgeGroups(edgeGroupIDs []portainer.EdgeGroupID, datastore
|
||||||
// edgeGroupRelatedToEndpoint returns true if edgeGroup is associated with environment(endpoint)
|
// edgeGroupRelatedToEndpoint returns true if edgeGroup is associated with environment(endpoint)
|
||||||
func edgeGroupRelatedToEndpoint(edgeGroup *portainer.EdgeGroup, endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup) bool {
|
func edgeGroupRelatedToEndpoint(edgeGroup *portainer.EdgeGroup, endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup) bool {
|
||||||
if !edgeGroup.Dynamic {
|
if !edgeGroup.Dynamic {
|
||||||
return slices.Contains(edgeGroup.Endpoints, endpoint.ID)
|
return edgeGroup.EndpointIDs.Contains(endpoint.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
endpointTags := tag.Set(endpoint.TagIDs)
|
endpointTags := tag.Set(endpoint.TagIDs)
|
||||||
|
|
104
api/internal/edge/edgegroup_benchmark_test.go
Normal file
104
api/internal/edge/edgegroup_benchmark_test.go
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
package edge
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/datastore"
|
||||||
|
"github.com/portainer/portainer/api/roar"
|
||||||
|
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const n = 1_000_000
|
||||||
|
|
||||||
|
func BenchmarkWriteEdgeGroupOld(b *testing.B) {
|
||||||
|
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||||
|
|
||||||
|
_, store := datastore.MustNewTestStore(b, false, false)
|
||||||
|
|
||||||
|
var endpointIDs []portainer.EndpointID
|
||||||
|
|
||||||
|
for i := range n {
|
||||||
|
endpointIDs = append(endpointIDs, portainer.EndpointID(i+1))
|
||||||
|
}
|
||||||
|
|
||||||
|
for b.Loop() {
|
||||||
|
err := store.EdgeGroup().Create(&portainer.EdgeGroup{
|
||||||
|
Name: "Test Edge Group",
|
||||||
|
Endpoints: endpointIDs,
|
||||||
|
})
|
||||||
|
require.NoError(b, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkWriteEdgeGroupNew(b *testing.B) {
|
||||||
|
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||||
|
|
||||||
|
_, store := datastore.MustNewTestStore(b, false, false)
|
||||||
|
|
||||||
|
var ts []portainer.EndpointID
|
||||||
|
|
||||||
|
for i := range n {
|
||||||
|
ts = append(ts, portainer.EndpointID(i+1))
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointIDs := roar.FromSlice(ts)
|
||||||
|
|
||||||
|
for b.Loop() {
|
||||||
|
err := store.EdgeGroup().Create(&portainer.EdgeGroup{
|
||||||
|
Name: "Test Edge Group",
|
||||||
|
EndpointIDs: endpointIDs,
|
||||||
|
})
|
||||||
|
require.NoError(b, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkReadEdgeGroupOld(b *testing.B) {
|
||||||
|
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||||
|
|
||||||
|
_, store := datastore.MustNewTestStore(b, false, false)
|
||||||
|
|
||||||
|
var endpointIDs []portainer.EndpointID
|
||||||
|
|
||||||
|
for i := range n {
|
||||||
|
endpointIDs = append(endpointIDs, portainer.EndpointID(i+1))
|
||||||
|
}
|
||||||
|
|
||||||
|
err := store.EdgeGroup().Create(&portainer.EdgeGroup{
|
||||||
|
Name: "Test Edge Group",
|
||||||
|
Endpoints: endpointIDs,
|
||||||
|
})
|
||||||
|
require.NoError(b, err)
|
||||||
|
|
||||||
|
for b.Loop() {
|
||||||
|
_, err := store.EdgeGroup().ReadAll()
|
||||||
|
require.NoError(b, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkReadEdgeGroupNew(b *testing.B) {
|
||||||
|
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||||
|
|
||||||
|
_, store := datastore.MustNewTestStore(b, false, false)
|
||||||
|
|
||||||
|
var ts []portainer.EndpointID
|
||||||
|
|
||||||
|
for i := range n {
|
||||||
|
ts = append(ts, portainer.EndpointID(i+1))
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointIDs := roar.FromSlice(ts)
|
||||||
|
|
||||||
|
err := store.EdgeGroup().Create(&portainer.EdgeGroup{
|
||||||
|
Name: "Test Edge Group",
|
||||||
|
EndpointIDs: endpointIDs,
|
||||||
|
})
|
||||||
|
require.NoError(b, err)
|
||||||
|
|
||||||
|
for b.Loop() {
|
||||||
|
_, err := store.EdgeGroup().ReadAll()
|
||||||
|
require.NoError(b, err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,17 +7,18 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
gittypes "github.com/portainer/portainer/api/git/types"
|
||||||
|
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
||||||
|
"github.com/portainer/portainer/api/roar"
|
||||||
|
"github.com/portainer/portainer/pkg/featureflags"
|
||||||
|
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
||||||
|
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
"github.com/docker/docker/api/types/image"
|
"github.com/docker/docker/api/types/image"
|
||||||
"github.com/docker/docker/api/types/network"
|
"github.com/docker/docker/api/types/network"
|
||||||
"github.com/docker/docker/api/types/system"
|
"github.com/docker/docker/api/types/system"
|
||||||
"github.com/docker/docker/api/types/volume"
|
"github.com/docker/docker/api/types/volume"
|
||||||
gittypes "github.com/portainer/portainer/api/git/types"
|
|
||||||
models "github.com/portainer/portainer/api/http/models/kubernetes"
|
|
||||||
"github.com/portainer/portainer/pkg/featureflags"
|
|
||||||
httperror "github.com/portainer/portainer/pkg/libhttp/error"
|
|
||||||
"github.com/segmentio/encoding/json"
|
"github.com/segmentio/encoding/json"
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/version"
|
"k8s.io/apimachinery/pkg/version"
|
||||||
|
@ -269,8 +270,11 @@ type (
|
||||||
Name string `json:"Name"`
|
Name string `json:"Name"`
|
||||||
Dynamic bool `json:"Dynamic"`
|
Dynamic bool `json:"Dynamic"`
|
||||||
TagIDs []TagID `json:"TagIds"`
|
TagIDs []TagID `json:"TagIds"`
|
||||||
Endpoints []EndpointID `json:"Endpoints"`
|
EndpointIDs roar.Roar[EndpointID] `json:"EndpointIds"`
|
||||||
PartialMatch bool `json:"PartialMatch"`
|
PartialMatch bool `json:"PartialMatch"`
|
||||||
|
|
||||||
|
// Deprecated: only used for API responses
|
||||||
|
Endpoints []EndpointID `json:"Endpoints"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// EdgeGroupID represents an Edge group identifier
|
// EdgeGroupID represents an Edge group identifier
|
||||||
|
|
145
api/roar/roar.go
Normal file
145
api/roar/roar.go
Normal file
|
@ -0,0 +1,145 @@
|
||||||
|
package roar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/RoaringBitmap/roaring/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Roar[T ~int] struct {
|
||||||
|
rb *roaring.Bitmap
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate iterates over the bitmap, calling the given callback with each value in the bitmap. If the callback returns
|
||||||
|
// false, the iteration is halted.
|
||||||
|
// The iteration results are undefined if the bitmap is modified (e.g., with Add or Remove).
|
||||||
|
// There is no guarantee as to what order the values will be iterated.
|
||||||
|
func (r *Roar[T]) Iterate(f func(T) bool) {
|
||||||
|
if r.rb == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.rb.Iterate(func(e uint32) bool {
|
||||||
|
return f(T(e))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Len returns the number of elements contained in the bitmap
|
||||||
|
func (r *Roar[T]) Len() int {
|
||||||
|
if r.rb == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return int(r.rb.GetCardinality())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove removes the given element from the bitmap
|
||||||
|
func (r *Roar[T]) Remove(e T) {
|
||||||
|
if r.rb == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
r.rb.Remove(uint32(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds the given element to the bitmap
|
||||||
|
func (r *Roar[T]) Add(e T) {
|
||||||
|
if r.rb == nil {
|
||||||
|
r.rb = roaring.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
r.rb.AddInt(int(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contains returns whether the bitmap contains the given element or not
|
||||||
|
func (r *Roar[T]) Contains(e T) bool {
|
||||||
|
if r.rb == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.rb.ContainsInt(int(e))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Union combines the elements of the given bitmap with this bitmap
|
||||||
|
func (r *Roar[T]) Union(other Roar[T]) {
|
||||||
|
if other.rb == nil {
|
||||||
|
return
|
||||||
|
} else if r.rb == nil {
|
||||||
|
r.rb = roaring.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
r.rb.Or(other.rb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intersection modifies this bitmap to only contain elements that are also in the other bitmap
|
||||||
|
func (r *Roar[T]) Intersection(other Roar[T]) {
|
||||||
|
if other.rb == nil {
|
||||||
|
if r.rb != nil {
|
||||||
|
r.rb.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.rb == nil {
|
||||||
|
r.rb = roaring.New()
|
||||||
|
}
|
||||||
|
|
||||||
|
r.rb.And(other.rb)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToSlice converts the bitmap to a slice of elements
|
||||||
|
func (r *Roar[T]) ToSlice() []T {
|
||||||
|
if r.rb == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
slice := make([]T, 0, r.rb.GetCardinality())
|
||||||
|
r.rb.Iterate(func(e uint32) bool {
|
||||||
|
slice = append(slice, T(e))
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return slice
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Roar[T]) MarshalJSON() ([]byte, error) {
|
||||||
|
if r.rb == nil {
|
||||||
|
return []byte("null"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r.rb.RunOptimize()
|
||||||
|
|
||||||
|
buf, err := r.rb.ToBase64()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to encode roaring bitmap: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Appendf(nil, `"%s"`, buf), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Roar[T]) UnmarshalJSON(data []byte) error {
|
||||||
|
if len(data) == 0 || string(data) == "null" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
r.rb = roaring.New()
|
||||||
|
|
||||||
|
_, err := r.rb.FromBase64(string(data[1 : len(data)-1]))
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromSlice creates a Roar by adding all elements from the provided slices
|
||||||
|
func FromSlice[T ~int](ess ...[]T) Roar[T] {
|
||||||
|
var r Roar[T]
|
||||||
|
|
||||||
|
for _, es := range ess {
|
||||||
|
for _, e := range es {
|
||||||
|
r.Add(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
123
api/roar/roar_test.go
Normal file
123
api/roar/roar_test.go
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
package roar
|
||||||
|
|
||||||
|
import (
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRoar(t *testing.T) {
|
||||||
|
r := Roar[int]{}
|
||||||
|
require.Equal(t, 0, r.Len())
|
||||||
|
|
||||||
|
r.Add(1)
|
||||||
|
require.Equal(t, 1, r.Len())
|
||||||
|
require.True(t, r.Contains(1))
|
||||||
|
require.False(t, r.Contains(2))
|
||||||
|
|
||||||
|
r.Add(2)
|
||||||
|
require.Equal(t, 2, r.Len())
|
||||||
|
require.True(t, r.Contains(2))
|
||||||
|
|
||||||
|
r.Remove(1)
|
||||||
|
require.Equal(t, 1, r.Len())
|
||||||
|
require.False(t, r.Contains(1))
|
||||||
|
|
||||||
|
s := FromSlice([]int{3, 4, 5})
|
||||||
|
require.Equal(t, 3, s.Len())
|
||||||
|
require.True(t, s.Contains(3))
|
||||||
|
require.True(t, s.Contains(4))
|
||||||
|
require.True(t, s.Contains(5))
|
||||||
|
|
||||||
|
r.Union(s)
|
||||||
|
require.Equal(t, 4, r.Len())
|
||||||
|
require.True(t, r.Contains(2))
|
||||||
|
require.True(t, r.Contains(3))
|
||||||
|
require.True(t, r.Contains(4))
|
||||||
|
require.True(t, r.Contains(5))
|
||||||
|
|
||||||
|
r.Iterate(func(id int) bool {
|
||||||
|
require.True(t, slices.Contains([]int{2, 3, 4, 5}, id))
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
rSlice := r.ToSlice()
|
||||||
|
require.EqualValues(t, []int{2, 3, 4, 5}, rSlice)
|
||||||
|
|
||||||
|
r.Intersection(FromSlice([]int{4}))
|
||||||
|
require.Equal(t, 1, r.Len())
|
||||||
|
require.True(t, r.Contains(4))
|
||||||
|
require.False(t, r.Contains(2))
|
||||||
|
require.False(t, r.Contains(3))
|
||||||
|
require.False(t, r.Contains(5))
|
||||||
|
|
||||||
|
b, err := r.MarshalJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEqual(t, "null", string(b))
|
||||||
|
require.True(t, strings.HasPrefix(string(b), `"`))
|
||||||
|
require.True(t, strings.HasSuffix(string(b), `"`))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNilSafety(t *testing.T) {
|
||||||
|
var r, s, u Roar[int]
|
||||||
|
|
||||||
|
r.Iterate(func(id int) bool {
|
||||||
|
require.Fail(t, "should not iterate over nil Roar")
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
b, err := r.MarshalJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, "null", string(b))
|
||||||
|
|
||||||
|
err = r.UnmarshalJSON([]byte("null"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 0, r.Len())
|
||||||
|
|
||||||
|
r.Contains(1)
|
||||||
|
r.Remove(1)
|
||||||
|
|
||||||
|
require.Equal(t, 0, r.Len())
|
||||||
|
require.Empty(t, r.ToSlice())
|
||||||
|
|
||||||
|
r.Add(1)
|
||||||
|
require.Equal(t, 1, r.Len())
|
||||||
|
require.False(t, r.Contains(2))
|
||||||
|
|
||||||
|
s.Union(r)
|
||||||
|
require.Equal(t, 1, s.Len())
|
||||||
|
require.True(t, s.Contains(1))
|
||||||
|
|
||||||
|
r.Union(u)
|
||||||
|
require.Equal(t, 1, r.Len())
|
||||||
|
require.True(t, r.Contains(1))
|
||||||
|
|
||||||
|
s.Intersection(u)
|
||||||
|
require.Equal(t, 0, s.Len())
|
||||||
|
|
||||||
|
u.Intersection(r)
|
||||||
|
require.Equal(t, 0, u.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSON(t *testing.T) {
|
||||||
|
var r, u Roar[int]
|
||||||
|
|
||||||
|
r.Add(1)
|
||||||
|
r.Add(2)
|
||||||
|
r.Add(3)
|
||||||
|
|
||||||
|
b, err := r.MarshalJSON()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotEqual(t, "null", string(b))
|
||||||
|
|
||||||
|
err = u.UnmarshalJSON(b)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, 3, u.Len())
|
||||||
|
require.True(t, u.Contains(1))
|
||||||
|
require.True(t, u.Contains(2))
|
||||||
|
require.True(t, u.Contains(3))
|
||||||
|
}
|
3
go.mod
3
go.mod
|
@ -5,6 +5,7 @@ go 1.24.4
|
||||||
require (
|
require (
|
||||||
github.com/Masterminds/semver v1.5.0
|
github.com/Masterminds/semver v1.5.0
|
||||||
github.com/Microsoft/go-winio v0.6.2
|
github.com/Microsoft/go-winio v0.6.2
|
||||||
|
github.com/RoaringBitmap/roaring/v2 v2.5.0
|
||||||
github.com/VictoriaMetrics/fastcache v1.12.0
|
github.com/VictoriaMetrics/fastcache v1.12.0
|
||||||
github.com/aws/aws-sdk-go-v2 v1.30.3
|
github.com/aws/aws-sdk-go-v2 v1.30.3
|
||||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.27
|
github.com/aws/aws-sdk-go-v2/credentials v1.17.27
|
||||||
|
@ -102,6 +103,7 @@ require (
|
||||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
|
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect
|
||||||
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
|
github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/bits-and-blooms/bitset v1.12.0 // indirect
|
||||||
github.com/blang/semver/v4 v4.0.0 // indirect
|
github.com/blang/semver/v4 v4.0.0 // indirect
|
||||||
github.com/buger/goterm v1.0.4 // indirect
|
github.com/buger/goterm v1.0.4 // indirect
|
||||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||||
|
@ -225,6 +227,7 @@ require (
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
|
github.com/mschoch/smat v0.2.0 // indirect
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
|
||||||
github.com/opencontainers/runtime-spec v1.2.1 // indirect
|
github.com/opencontainers/runtime-spec v1.2.1 // indirect
|
||||||
|
|
7
go.sum
7
go.sum
|
@ -38,6 +38,8 @@ github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63n
|
||||||
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
|
||||||
github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk=
|
github.com/ProtonMail/go-crypto v1.1.3 h1:nRBOetoydLeUb4nHajyO2bKqMLfWQ/ZPwkXqXxPxCFk=
|
||||||
github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
github.com/ProtonMail/go-crypto v1.1.3/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||||
|
github.com/RoaringBitmap/roaring/v2 v2.5.0 h1:TJ45qCM7D7fIEBwKd9zhoR0/S1egfnSSIzLU1e1eYLY=
|
||||||
|
github.com/RoaringBitmap/roaring/v2 v2.5.0/go.mod h1:FiJcsfkGje/nZBZgCu0ZxCPOKD/hVXDS2dXi7/eUFE0=
|
||||||
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
||||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
|
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs=
|
||||||
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ=
|
||||||
|
@ -100,6 +102,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw=
|
github.com/bitly/go-hostpool v0.1.0/go.mod h1:4gOCgp6+NZnVqlKyZ/iBZFTAJKembaVENUpMkpg42fw=
|
||||||
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
|
github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA=
|
||||||
|
github.com/bits-and-blooms/bitset v1.12.0 h1:U/q1fAF7xXRhFCrhROzIfffYnu+dlS38vCZtmFVPHmA=
|
||||||
|
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||||
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
|
||||||
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||||
|
@ -548,6 +552,8 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/
|
||||||
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
|
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
|
||||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||||
|
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||||
|
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||||
|
@ -945,6 +951,7 @@ gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue