1
0
Fork 0
mirror of https://github.com/portainer/portainer.git synced 2025-07-31 19:35:21 +02:00

fix(edgegroups): convert the related endpoint IDs to roaring bitmaps to increase performance BE-12053 (#903)

This commit is contained in:
andres-portainer 2025-07-21 21:31:13 -03:00 committed by GitHub
parent caf382b64c
commit 937456596a
32 changed files with 1041 additions and 133 deletions

View file

@ -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
} }

View file

@ -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,
} }

View 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
}

View file

@ -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.
} }

View 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 {

View file

@ -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)
} }

View 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)
}

View file

@ -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

View 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)
}

View file

@ -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,8 +82,8 @@ 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 {
endpointIDs, err := GetEndpointsByTags(tx, edgeGroup.TagIDs, edgeGroup.PartialMatch) endpointIDs, err := GetEndpointsByTags(tx, edgeGroup.TagIDs, edgeGroup.PartialMatch)
@ -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))

View file

@ -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)
}

View file

@ -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 {

View 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)
}

View file

@ -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,
} }

View file

@ -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,
} }

View file

@ -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,
} }

View file

@ -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"
@ -366,8 +367,8 @@ func TestEdgeJobsResponse(t *testing.T) {
unrelatedEndpoint := localCreateEndpoint(80, nil) unrelatedEndpoint := localCreateEndpoint(80, nil)
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)

View file

@ -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")

View file

@ -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) {
@ -42,9 +43,9 @@ 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")
} }
} }

View file

@ -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++
} }
return true
})
if innerErr != nil {
return nil, innerErr
} }
envIds = envIds[:n]
} }
uniqueIds := slicesx.Unique(envIds) filteredEndpoints := filteredEndpointsByIds(endpoints, envIds)
filteredEndpoints := filteredEndpointsByIds(endpoints, uniqueIds)
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++
} }

View file

@ -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,
@ -222,11 +224,11 @@ func BenchmarkFilterEndpointsBySearchCriteria_FullMatch(b *testing.B) {
edgeGroups := []portainer.EdgeGroup{} edgeGroups := []portainer.EdgeGroup{}
for i := range 1000 { for i := range 1000 {
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))
}

View file

@ -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,10 +18,8 @@ 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
}
} }
} }
@ -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
} }

View file

@ -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"
) )
@ -14,10 +15,9 @@ func Test_updateEdgeGroups(t *testing.T) {
groups := make([]portainer.EdgeGroup, len(names)) groups := make([]portainer.EdgeGroup, len(names))
for index, name := range names { for index, name := range names {
group := &portainer.EdgeGroup{ group := &portainer.EdgeGroup{
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)

View file

@ -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 {

View file

@ -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"
@ -127,9 +128,9 @@ func TestHandler_tagDelete(t *testing.T) {
require.NoError(t, store.EdgeGroup().Create(dynamicEdgeGroup)) require.NoError(t, store.EdgeGroup().Create(dynamicEdgeGroup))
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)
}
}) })
} }

View file

@ -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)

View 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)
}
}

View file

@ -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"
@ -265,12 +266,15 @@ type (
// EdgeGroup represents an Edge group // EdgeGroup represents an Edge group
EdgeGroup struct { EdgeGroup struct {
// EdgeGroup Identifier // EdgeGroup Identifier
ID EdgeGroupID `json:"Id" example:"1"` ID EdgeGroupID `json:"Id" example:"1"`
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
View 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
View 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
View file

@ -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
View file

@ -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=