Compare commits

...

5 Commits

Author SHA1 Message Date
Oscar Zhou 8cbd23c059 version: bump version to 2.21.3 (#12300) 2024-10-08 08:12:46 +13:00
andres-portainer 3800a958da fix(endpoints): optimize the search performance BE-11267 (#12264) 2024-10-01 15:14:04 -03:00
Oscar Zhou 09348b8a25 version: bump version to 2.21.2 (#12244) 2024-09-24 07:54:26 +12:00
Nik Wakelin 1ef9c249b7 chore(branding): Backport branding changes to 2 21 (#12243) 2024-09-23 10:33:55 +12:00
Oscar Zhou 33ac61c600 fix: golang lint error [BE-11235] (#12215) 2024-09-17 08:08:26 +12:00
20 changed files with 212 additions and 118 deletions
@@ -941,6 +941,6 @@
}
],
"version": {
"VERSION": "{\"SchemaVersion\":\"2.21.1\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
"VERSION": "{\"SchemaVersion\":\"2.21.3\",\"MigratorCount\":0,\"Edition\":1,\"InstanceID\":\"463d5c47-0ea5-4aca-85b1-405ceefee254\"}"
}
}
@@ -78,7 +78,7 @@ func (handler *Handler) imagesList(w http.ResponseWriter, r *http.Request) *http
imagesList := make([]ImageResponse, len(images))
for i, image := range images {
if (image.RepoTags == nil || len(image.RepoTags) == 0) && (image.RepoDigests != nil && len(image.RepoDigests) > 0) {
if len(image.RepoTags) == 0 && len(image.RepoDigests) > 0 {
for _, repoDigest := range image.RepoDigests {
image.RepoTags = append(image.RepoTags, repoDigest[0:strings.Index(repoDigest, "@")]+":<none>")
}
@@ -154,7 +154,7 @@ func TestMissingEdgeIdentifier(t *testing.T) {
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d without Edge identifier", http.StatusForbidden, rec.Code))
t.Fatalf("expected a %d response, found: %d without Edge identifier", http.StatusForbidden, rec.Code)
}
}
@@ -179,7 +179,7 @@ func TestWithEndpoints(t *testing.T) {
handler.ServeHTTP(rec, req)
if rec.Code != test.expectedStatusCode {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d for endpoint ID: %d", test.expectedStatusCode, rec.Code, test.endpoint.ID))
t.Fatalf("expected a %d response, found: %d for endpoint ID: %d", test.expectedStatusCode, rec.Code, test.endpoint.ID)
}
}
}
@@ -219,7 +219,7 @@ func TestLastCheckInDateIncreases(t *testing.T) {
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
t.Fatalf("expected a %d response, found: %d", http.StatusOK, rec.Code)
}
updatedEndpoint, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID)
@@ -262,7 +262,7 @@ func TestEmptyEdgeIdWithAgentPlatformHeader(t *testing.T) {
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d with empty edge ID", http.StatusOK, rec.Code))
t.Fatalf("expected a %d response, found: %d with empty edge ID", http.StatusOK, rec.Code)
}
updatedEndpoint, err := handler.DataStore.Endpoint().Endpoint(endpoint.ID)
@@ -326,7 +326,7 @@ func TestEdgeStackStatus(t *testing.T) {
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
t.Fatalf("expected a %d response, found: %d", http.StatusOK, rec.Code)
}
var data endpointEdgeStatusInspectResponse
@@ -391,7 +391,7 @@ func TestEdgeJobsResponse(t *testing.T) {
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf(fmt.Sprintf("expected a %d response, found: %d", http.StatusOK, rec.Code))
t.Fatalf("expected a %d response, found: %d", http.StatusOK, rec.Code)
}
var data endpointEdgeStatusInspectResponse
@@ -96,8 +96,8 @@ func (handler *Handler) updateEndpointGroup(tx dataservices.DataStoreTx, endpoin
payloadTagSet := tag.Set(payload.TagIDs)
endpointGroupTagSet := tag.Set((endpointGroup.TagIDs))
union := tag.Union(payloadTagSet, endpointGroupTagSet)
intersection := tag.Intersection(payloadTagSet, endpointGroupTagSet)
tagsChanged = len(union) > len(intersection)
intersection := tag.IntersectionCount(payloadTagSet, endpointGroupTagSet)
tagsChanged = len(union) > intersection
if tagsChanged {
removeTags := tag.Difference(endpointGroupTagSet, payloadTagSet)
+17 -28
View File
@@ -193,7 +193,7 @@ func (handler *Handler) filterEndpointsByQuery(
return nil, 0, errors.WithMessage(err, "Unable to retrieve tags from the database")
}
tagsMap := make(map[portainer.TagID]string)
tagsMap := make(map[portainer.TagID]string, len(tags))
for _, tag := range tags {
tagsMap[tag.ID] = tag.Name
}
@@ -302,8 +302,7 @@ func filterEndpointsBySearchCriteria(
) []portainer.Endpoint {
n := 0
for _, endpoint := range endpoints {
endpointTags := convertTagIDsToTags(tagsMap, endpoint.TagIDs)
if endpointMatchSearchCriteria(&endpoint, endpointTags, searchCriteria) {
if endpointMatchSearchCriteria(&endpoint, tagsMap, searchCriteria) {
endpoints[n] = endpoint
n++
@@ -317,7 +316,7 @@ func filterEndpointsBySearchCriteria(
continue
}
if edgeGroupMatchSearchCriteria(&endpoint, edgeGroups, searchCriteria, endpoints, endpointGroups) {
if edgeGroupMatchSearchCriteria(&endpoint, edgeGroups, searchCriteria, endpointGroups) {
endpoints[n] = endpoint
n++
@@ -363,7 +362,7 @@ func filterEndpointsByStatuses(endpoints []portainer.Endpoint, statuses []portai
return endpoints[:n]
}
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, searchCriteria string) bool {
func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tagsMap map[portainer.TagID]string, searchCriteria string) bool {
if strings.Contains(strings.ToLower(endpoint.Name), searchCriteria) {
return true
}
@@ -378,8 +377,8 @@ func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, se
return true
}
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
for _, tagID := range endpoint.TagIDs {
if strings.Contains(strings.ToLower(tagsMap[tagID]), searchCriteria) {
return true
}
}
@@ -389,16 +388,17 @@ func endpointMatchSearchCriteria(endpoint *portainer.Endpoint, tags []string, se
func endpointGroupMatchSearchCriteria(endpoint *portainer.Endpoint, endpointGroups []portainer.EndpointGroup, tagsMap map[portainer.TagID]string, searchCriteria string) bool {
for _, group := range endpointGroups {
if group.ID == endpoint.GroupID {
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
return true
}
if group.ID != endpoint.GroupID {
continue
}
tags := convertTagIDsToTags(tagsMap, group.TagIDs)
for _, tag := range tags {
if strings.Contains(strings.ToLower(tag), searchCriteria) {
return true
}
if strings.Contains(strings.ToLower(group.Name), searchCriteria) {
return true
}
for _, tagID := range group.TagIDs {
if strings.Contains(strings.ToLower(tagsMap[tagID]), searchCriteria) {
return true
}
}
}
@@ -411,11 +411,10 @@ func edgeGroupMatchSearchCriteria(
endpoint *portainer.Endpoint,
edgeGroups []portainer.EdgeGroup,
searchCriteria string,
endpoints []portainer.Endpoint,
endpointGroups []portainer.EndpointGroup,
) bool {
for _, edgeGroup := range edgeGroups {
relatedEndpointIDs := edge.EdgeGroupRelatedEndpoints(&edgeGroup, endpoints, endpointGroups)
relatedEndpointIDs := edge.EdgeGroupRelatedEndpoints(&edgeGroup, []portainer.Endpoint{*endpoint}, endpointGroups)
for _, endpointID := range relatedEndpointIDs {
if endpointID == endpoint.ID {
@@ -446,16 +445,6 @@ func filterEndpointsByTypes(endpoints []portainer.Endpoint, endpointTypes []port
return endpoints[:n]
}
func convertTagIDsToTags(tagsMap map[portainer.TagID]string, tagIDs []portainer.TagID) []string {
tags := make([]string, 0, len(tagIDs))
for _, tagID := range tagIDs {
tags = append(tags, tagsMap[tagID])
}
return tags
}
func filteredEndpointsByTags(endpoints []portainer.Endpoint, tagIDs []portainer.TagID, endpointGroups []portainer.EndpointGroup, partialMatch bool) []portainer.Endpoint {
n := 0
for _, endpoint := range endpoints {
+98
View File
@@ -1,6 +1,7 @@
package endpoints
import (
"strconv"
"testing"
portainer "github.com/portainer/portainer/api"
@@ -148,6 +149,103 @@ func Test_Filter_excludeIDs(t *testing.T) {
runTests(tests, t, handler, environments)
}
func BenchmarkFilterEndpointsBySearchCriteria_PartialMatch(b *testing.B) {
n := 10000
endpointIDs := []portainer.EndpointID{}
endpoints := []portainer.Endpoint{}
for i := 0; i < n; i++ {
endpoints = append(endpoints, portainer.Endpoint{
ID: portainer.EndpointID(i + 1),
Name: "endpoint-" + strconv.Itoa(i+1),
GroupID: 1,
TagIDs: []portainer.TagID{1},
Type: portainer.EdgeAgentOnDockerEnvironment,
})
endpointIDs = append(endpointIDs, portainer.EndpointID(i+1))
}
endpointGroups := []portainer.EndpointGroup{}
edgeGroups := []portainer.EdgeGroup{}
for i := 0; i < 1000; i++ {
edgeGroups = append(edgeGroups, portainer.EdgeGroup{
ID: portainer.EdgeGroupID(i + 1),
Name: "edge-group-" + strconv.Itoa(i+1),
Endpoints: append([]portainer.EndpointID{}, endpointIDs...),
Dynamic: true,
TagIDs: []portainer.TagID{1, 2, 3},
PartialMatch: true,
})
}
tagsMap := map[portainer.TagID]string{}
for i := 0; i < 10; i++ {
tagsMap[portainer.TagID(i+1)] = "tag-" + strconv.Itoa(i+1)
}
searchString := "edge-group"
b.ResetTimer()
for i := 0; i < b.N; i++ {
e := filterEndpointsBySearchCriteria(endpoints, endpointGroups, edgeGroups, tagsMap, searchString)
if len(e) != n {
b.FailNow()
}
}
}
func BenchmarkFilterEndpointsBySearchCriteria_FullMatch(b *testing.B) {
n := 10000
endpointIDs := []portainer.EndpointID{}
endpoints := []portainer.Endpoint{}
for i := 0; i < n; i++ {
endpoints = append(endpoints, portainer.Endpoint{
ID: portainer.EndpointID(i + 1),
Name: "endpoint-" + strconv.Itoa(i+1),
GroupID: 1,
TagIDs: []portainer.TagID{1, 2, 3},
Type: portainer.EdgeAgentOnDockerEnvironment,
})
endpointIDs = append(endpointIDs, portainer.EndpointID(i+1))
}
endpointGroups := []portainer.EndpointGroup{}
edgeGroups := []portainer.EdgeGroup{}
for i := 0; i < 1000; i++ {
edgeGroups = append(edgeGroups, portainer.EdgeGroup{
ID: portainer.EdgeGroupID(i + 1),
Name: "edge-group-" + strconv.Itoa(i+1),
Endpoints: append([]portainer.EndpointID{}, endpointIDs...),
Dynamic: true,
TagIDs: []portainer.TagID{1},
})
}
tagsMap := map[portainer.TagID]string{}
for i := 0; i < 10; i++ {
tagsMap[portainer.TagID(i+1)] = "tag-" + strconv.Itoa(i+1)
}
searchString := "edge-group"
b.ResetTimer()
for i := 0; i < b.N; i++ {
e := filterEndpointsBySearchCriteria(endpoints, endpointGroups, edgeGroups, tagsMap, searchString)
if len(e) != n {
b.FailNow()
}
}
}
func runTests(tests []filterTest, t *testing.T, handler *Handler, endpoints []portainer.Endpoint) {
for _, test := range tests {
t.Run(test.title, func(t *testing.T) {
+1 -1
View File
@@ -85,7 +85,7 @@ type Handler struct {
}
// @title PortainerCE API
// @version 2.21.1
// @version 2.21.3
// @description.markdown api-description.md
// @termsOfService
+2 -1
View File
@@ -1,6 +1,7 @@
package stacks
import (
"errors"
"fmt"
"net/http"
"time"
@@ -95,7 +96,7 @@ func (handler *Handler) stackAssociate(w http.ResponseWriter, r *http.Request) *
}
if !canManage {
errMsg := "Stack management is disabled for non-admin users"
return httperror.Forbidden(errMsg, fmt.Errorf(errMsg))
return httperror.Forbidden(errMsg, errors.New(errMsg))
}
stack.EndpointID = portainer.EndpointID(endpointID)
+2 -2
View File
@@ -111,7 +111,7 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
}
if !canManage {
errMsg := "stack deletion is disabled for non-admin users"
return httperror.Forbidden(errMsg, fmt.Errorf(errMsg))
return httperror.Forbidden(errMsg, errors.New(errMsg))
}
// stop scheduler updates of the stack before removal
@@ -338,7 +338,7 @@ func (handler *Handler) stackDeleteKubernetesByName(w http.ResponseWriter, r *ht
}
if !canManage {
errMsg := "stack deletion is disabled for non-admin users"
return httperror.Forbidden(errMsg, fmt.Errorf(errMsg))
return httperror.Forbidden(errMsg, errors.New(errMsg))
}
stacksToDelete = append(stacksToDelete, stack)
+3 -4
View File
@@ -77,6 +77,7 @@ func edgeGroupRelatedToEndpoint(edgeGroup *portainer.EdgeGroup, endpoint *portai
return true
}
}
return false
}
@@ -84,12 +85,10 @@ func edgeGroupRelatedToEndpoint(edgeGroup *portainer.EdgeGroup, endpoint *portai
if endpointGroup.TagIDs != nil {
endpointTags = tag.Union(endpointTags, tag.Set(endpointGroup.TagIDs))
}
edgeGroupTags := tag.Set(edgeGroup.TagIDs)
if edgeGroup.PartialMatch {
intersection := tag.Intersection(endpointTags, edgeGroupTags)
return len(intersection) != 0
return tag.PartialMatch(edgeGroup.TagIDs, endpointTags)
}
return tag.FullMatch(edgeGroupTags, endpointTags)
return tag.FullMatch(edgeGroup.TagIDs, endpointTags)
}
+26 -27
View File
@@ -1,64 +1,63 @@
package tag
import portainer "github.com/portainer/portainer/api"
import (
portainer "github.com/portainer/portainer/api"
)
type tagSet map[portainer.TagID]bool
type tagSet map[portainer.TagID]struct{}
// Set converts an array of ids to a set
func Set(tagIDs []portainer.TagID) tagSet {
set := map[portainer.TagID]bool{}
set := map[portainer.TagID]struct{}{}
for _, tagID := range tagIDs {
set[tagID] = true
set[tagID] = struct{}{}
}
return set
}
// Intersection returns a set intersection of the provided sets
func Intersection(sets ...tagSet) tagSet {
intersection := tagSet{}
if len(sets) == 0 {
return intersection
// IntersectionCount returns the element count of the intersection of the sets
func IntersectionCount(setA, setB tagSet) int {
if len(setA) > len(setB) {
setA, setB = setB, setA
}
setA := sets[0]
count := 0
for tag := range setA {
inAll := true
for _, setB := range sets {
if !setB[tag] {
inAll = false
break
}
}
if inAll {
intersection[tag] = true
if _, ok := setB[tag]; ok {
count++
}
}
return intersection
return count
}
// Union returns a set union of provided sets
func Union(sets ...tagSet) tagSet {
union := tagSet{}
for _, set := range sets {
for tag := range set {
union[tag] = true
union[tag] = struct{}{}
}
}
return union
}
// Contains return true if setA contains setB
func Contains(setA tagSet, setB tagSet) bool {
func Contains(setA tagSet, setB []portainer.TagID) bool {
if len(setA) == 0 || len(setB) == 0 {
return false
}
for tag := range setB {
if !setA[tag] {
for _, tag := range setB {
if _, ok := setA[tag]; !ok {
return false
}
}
return true
}
@@ -67,8 +66,8 @@ func Difference(setA tagSet, setB tagSet) tagSet {
set := tagSet{}
for tag := range setA {
if !setB[tag] {
set[tag] = true
if _, ok := setB[tag]; !ok {
set[tag] = struct{}{}
}
}
+11 -3
View File
@@ -1,11 +1,19 @@
package tag
import portainer "github.com/portainer/portainer/api"
// FullMatch returns true if environment tags matches all edge group tags
func FullMatch(edgeGroupTags tagSet, environmentTags tagSet) bool {
func FullMatch(edgeGroupTags []portainer.TagID, environmentTags tagSet) bool {
return Contains(environmentTags, edgeGroupTags)
}
// PartialMatch returns true if environment tags matches at least one edge group tag
func PartialMatch(edgeGroupTags tagSet, environmentTags tagSet) bool {
return len(Intersection(edgeGroupTags, environmentTags)) != 0
func PartialMatch(edgeGroupTags []portainer.TagID, environmentTags tagSet) bool {
for _, tagID := range edgeGroupTags {
if _, ok := environmentTags[tagID]; ok {
return true
}
}
return false
}
+17 -17
View File
@@ -9,49 +9,49 @@ import (
func TestFullMatch(t *testing.T) {
cases := []struct {
name string
edgeGroupTags tagSet
edgeGroupTags []portainer.TagID
environmentTag tagSet
expected bool
}{
{
name: "environment tag partially match edge group tags",
edgeGroupTags: Set([]portainer.TagID{1, 2, 3}),
edgeGroupTags: []portainer.TagID{1, 2, 3},
environmentTag: Set([]portainer.TagID{1, 2}),
expected: false,
},
{
name: "edge group tags equal to environment tags",
edgeGroupTags: Set([]portainer.TagID{1, 2}),
edgeGroupTags: []portainer.TagID{1, 2},
environmentTag: Set([]portainer.TagID{1, 2}),
expected: true,
},
{
name: "environment tags fully match edge group tags",
edgeGroupTags: Set([]portainer.TagID{1, 2}),
edgeGroupTags: []portainer.TagID{1, 2},
environmentTag: Set([]portainer.TagID{1, 2, 3}),
expected: true,
},
{
name: "environment tags do not match edge group tags",
edgeGroupTags: Set([]portainer.TagID{1, 2}),
edgeGroupTags: []portainer.TagID{1, 2},
environmentTag: Set([]portainer.TagID{3, 4}),
expected: false,
},
{
name: "edge group has no tags and environment has tags",
edgeGroupTags: Set([]portainer.TagID{}),
edgeGroupTags: []portainer.TagID{},
environmentTag: Set([]portainer.TagID{1, 2}),
expected: false,
},
{
name: "edge group has tags and environment has no tags",
edgeGroupTags: Set([]portainer.TagID{1, 2}),
edgeGroupTags: []portainer.TagID{1, 2},
environmentTag: Set([]portainer.TagID{}),
expected: false,
},
{
name: "both edge group and environment have no tags",
edgeGroupTags: Set([]portainer.TagID{}),
edgeGroupTags: []portainer.TagID{},
environmentTag: Set([]portainer.TagID{}),
expected: false,
},
@@ -70,55 +70,55 @@ func TestFullMatch(t *testing.T) {
func TestPartialMatch(t *testing.T) {
cases := []struct {
name string
edgeGroupTags tagSet
edgeGroupTags []portainer.TagID
environmentTag tagSet
expected bool
}{
{
name: "environment tags partially match edge group tags 1",
edgeGroupTags: Set([]portainer.TagID{1, 2, 3}),
edgeGroupTags: []portainer.TagID{1, 2, 3},
environmentTag: Set([]portainer.TagID{1, 2}),
expected: true,
},
{
name: "environment tags partially match edge group tags 2",
edgeGroupTags: Set([]portainer.TagID{1, 2, 3}),
edgeGroupTags: []portainer.TagID{1, 2, 3},
environmentTag: Set([]portainer.TagID{1, 4, 5}),
expected: true,
},
{
name: "edge group tags equal to environment tags",
edgeGroupTags: Set([]portainer.TagID{1, 2}),
edgeGroupTags: []portainer.TagID{1, 2},
environmentTag: Set([]portainer.TagID{1, 2}),
expected: true,
},
{
name: "environment tags fully match edge group tags",
edgeGroupTags: Set([]portainer.TagID{1, 2}),
edgeGroupTags: []portainer.TagID{1, 2},
environmentTag: Set([]portainer.TagID{1, 2, 3}),
expected: true,
},
{
name: "environment tags do not match edge group tags",
edgeGroupTags: Set([]portainer.TagID{1, 2}),
edgeGroupTags: []portainer.TagID{1, 2},
environmentTag: Set([]portainer.TagID{3, 4}),
expected: false,
},
{
name: "edge group has no tags and environment has tags",
edgeGroupTags: Set([]portainer.TagID{}),
edgeGroupTags: []portainer.TagID{},
environmentTag: Set([]portainer.TagID{1, 2}),
expected: false,
},
{
name: "edge group has tags and environment has no tags",
edgeGroupTags: Set([]portainer.TagID{1, 2}),
edgeGroupTags: []portainer.TagID{1, 2},
environmentTag: Set([]portainer.TagID{}),
expected: false,
},
{
name: "both edge group and environment have no tags",
edgeGroupTags: Set([]portainer.TagID{}),
edgeGroupTags: []portainer.TagID{},
environmentTag: Set([]portainer.TagID{}),
expected: false,
},
+17 -17
View File
@@ -7,49 +7,49 @@ import (
portainer "github.com/portainer/portainer/api"
)
func TestIntersection(t *testing.T) {
func TestIntersectionCount(t *testing.T) {
cases := []struct {
name string
setA tagSet
setB tagSet
expected tagSet
expected int
}{
{
name: "positive numbers set intersection",
setA: Set([]portainer.TagID{1, 2, 3, 4, 5}),
setB: Set([]portainer.TagID{4, 5, 6, 7}),
expected: Set([]portainer.TagID{4, 5}),
expected: 2,
},
{
name: "empty setA intersection",
setA: Set([]portainer.TagID{1, 2, 3}),
setB: Set([]portainer.TagID{}),
expected: Set([]portainer.TagID{}),
expected: 0,
},
{
name: "empty setB intersection",
setA: Set([]portainer.TagID{}),
setB: Set([]portainer.TagID{1, 2, 3}),
expected: Set([]portainer.TagID{}),
expected: 0,
},
{
name: "no common elements sets intersection",
setA: Set([]portainer.TagID{1, 2, 3}),
setB: Set([]portainer.TagID{4, 5, 6}),
expected: Set([]portainer.TagID{}),
expected: 0,
},
{
name: "equal sets intersection",
setA: Set([]portainer.TagID{1, 2, 3}),
setB: Set([]portainer.TagID{1, 2, 3}),
expected: Set([]portainer.TagID{1, 2, 3}),
expected: 3,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result := Intersection(tc.setA, tc.setB)
if !reflect.DeepEqual(result, tc.expected) {
result := IntersectionCount(tc.setA, tc.setB)
if result != tc.expected {
t.Errorf("Expected %v, got %v", tc.expected, result)
}
})
@@ -109,49 +109,49 @@ func TestContains(t *testing.T) {
cases := []struct {
name string
setA tagSet
setB tagSet
setB []portainer.TagID
expected bool
}{
{
name: "setA contains setB",
setA: Set([]portainer.TagID{1, 2, 3}),
setB: Set([]portainer.TagID{1, 2}),
setB: []portainer.TagID{1, 2},
expected: true,
},
{
name: "setA equals to setB",
setA: Set([]portainer.TagID{1, 2}),
setB: Set([]portainer.TagID{1, 2}),
setB: []portainer.TagID{1, 2},
expected: true,
},
{
name: "setA contains parts of setB",
setA: Set([]portainer.TagID{1, 2}),
setB: Set([]portainer.TagID{1, 2, 3}),
setB: []portainer.TagID{1, 2, 3},
expected: false,
},
{
name: "setA does not contain setB",
setA: Set([]portainer.TagID{1, 2}),
setB: Set([]portainer.TagID{3, 4}),
setB: []portainer.TagID{3, 4},
expected: false,
},
{
name: "setA is empty and setB is not empty",
setA: Set([]portainer.TagID{}),
setB: Set([]portainer.TagID{1, 2}),
setB: []portainer.TagID{1, 2},
expected: false,
},
{
name: "setA is not empty and setB is empty",
setA: Set([]portainer.TagID{1, 2}),
setB: Set([]portainer.TagID{}),
setB: []portainer.TagID{},
expected: false,
},
{
name: "setA is empty and setB is empty",
setA: Set([]portainer.TagID{}),
setB: Set([]portainer.TagID{}),
setB: []portainer.TagID{},
expected: false,
},
}
+1 -1
View File
@@ -1601,7 +1601,7 @@ type (
const (
// APIVersion is the version number of the Portainer API
APIVersion = "2.21.1"
APIVersion = "2.21.3"
// Edition is what this edition of Portainer is called
Edition = PortainerCE
// ComposeSyntaxMaxVersion is a maximum supported version of the docker compose syntax
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="409.333" height="166.751"><defs><clipPath id="a" clipPathUnits="userSpaceOnUse"><path d="M0 0h612v792H0Z"/></clipPath><clipPath id="b" clipPathUnits="userSpaceOnUse"><path d="M0 0h612v792H0Z"/></clipPath></defs><g style="fill:#09c;fill-opacity:1"><path d="M0 0c-6.127 1.862-10.58 7.517-10.58 14.209 0 6.763 4.548 12.465 10.768 14.279.637.189.472.59-.306.59-8.271 0-14.986-6.669-14.986-14.869S-8.389-.66-.118-.66C.66-.66.707-.212 0 0" style="fill:#09c;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="matrix(5.60732 0 0 -5.60732 84.693 163.05)"/></g><g style="fill:#000"><path d="M0 0a10 10 0 0 0-.071 1.202 11.797 11.797 0 0 0 11.806 11.805c6.173 0 8.011-2.757 8.247-2.568.259.188-2.239 5.655-9.473 5.655A11.796 11.796 0 0 1-1.296 4.289c0-1.508.283-2.946.801-4.265C-.283-.542.047-.542 0 0" style="fill:#09c;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="matrix(5.60732 0 0 -5.60732 41.355 102.799)"/></g><g style="fill:#09c;fill-opacity:1"><path d="M0 0c3.063 1.343 6.928 1.39 10.721.047 2.545-.895 4.03-2.168 4.148-2.097.212.094-1.485 2.757-4.525 3.912A10.79 10.79 0 0 1-.165.259C-.495 0-.377-.165 0 0" style="fill:#09c;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="matrix(5.60732 0 0 -5.60732 69.102 55.1)"/></g><g style="fill:#f93;fill-opacity:1"><path d="M0 0c0-.848-.707-1.555-1.555-1.555-.849 0-1.555.683-1.555 1.555 0 .848.683 1.555 1.555 1.555S0 .872 0 0" style="fill:#f93;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="matrix(5.60732 0 0 -5.60732 409.333 86.283)"/></g><path d="m525.457 729.914.188-2.074h3.276l-1.108 12.041h-4.877l-6.174-12.04h3.346l1.037 2.073zm-.165 2.333H522.3l2.57 5.207h.022zM533.469 733.096h.495l2.333 3.18h3.039l-3.228-4.052 1.98-4.383h-3.229l-1.296 3.417h-.471l-.73-3.417h-2.757l2.544 12.04h2.757z" style="fill:#f93;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="matrix(5.60732 0 0 -5.60732 -2820.89 4227.904)"/><g clip-path="url(#b)" style="fill:#f93;fill-opacity:1" transform="matrix(5.60732 0 0 -5.60732 -2820.89 4227.904)"><g style="fill:#f93;fill-opacity:1"><path d="M0 0h2.757l1.107 5.255C4.477 8.153 3.37 8.506.542 8.506c-1.979 0-3.888.024-4.43-2.591h2.757c.165.754.636.918 1.32.918 1.201 0 1.154-.494.989-1.272L.895 4.218H.778c-.095.966-1.32.942-2.098.942-2.002 0-3.181-.636-3.605-2.662-.447-2.144.566-2.616 2.498-2.616.966 0 2.262.189 2.71 1.343h.094Zm-.778 3.487c.896 0 1.485-.07 1.343-.777C.377 1.838 0 1.673-1.155 1.673c-.424 0-1.201 0-1.013.919.165.778.707.895 1.39.895" style="fill:#f93;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="translate(543.649 727.84)"/></g><g style="fill:#f93;fill-opacity:1"><path d="m0 0-.259-1.178h.118C.401-.188 1.508.094 2.451.094c1.178 0 2.356-.212 2.191-1.649h.118C5.16-.353 6.386.094 7.446.094c1.956 0 2.781-.801 2.356-2.757L8.577-8.436H5.82l1.037 4.878c.141.872.283 1.532-.778 1.532-1.084 0-1.437-.707-1.626-1.626L3.44-8.436H.683l1.084 5.114c.142.777.189 1.296-.777 1.296-1.131 0-1.485-.613-1.697-1.626L-1.72-8.436h-2.757L-2.686 0Z" style="fill:#f93;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="translate(551.99 736.277)"/></g><g style="fill:#f93;fill-opacity:1"><path d="M0 0h2.757l1.131 5.255c.613 2.898-.495 3.251-3.322 3.251-1.98 0-3.889.024-4.43-2.591h2.757c.164.754.636.918 1.319.918 1.202 0 1.155-.494.99-1.272L.919 4.218H.801c-.094.966-1.319.942-2.097.942-2.003 0-3.181-.636-3.605-2.662-.448-2.144.565-2.616 2.497-2.616.967 0 2.263.189 2.71 1.343h.095Zm-.754 3.487c.895 0 1.484-.07 1.343-.777-.188-.872-.565-1.037-1.72-1.037-.424 0-1.202 0-1.013.919.165.778.707.895 1.39.895" style="fill:#f93;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="translate(566.906 727.84)"/></g></g><path d="M573.669 727.84h-2.757l1.767 8.437h2.78z" style="fill:#f93;fill-opacity:1;fill-rule:nonzero;stroke:none" transform="matrix(5.60732 0 0 -5.60732 -2820.89 4227.904)"/></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 32 32"><path d="M9.545 14.42l-1.2-8.258L3.37 3.074l1.612 7.8 4.562 3.556zm1.38 9.443l-.852-5.823-4.356-3.63 1.17 5.648 4.038 3.804zm-3.383-.64l.862 4.165 3.596 3.817L11.386 27l-3.842-3.78zm11.644-1.806l-1.837-1.402.014.33a.19.19 0 0 1-.084.166l-1.386.934 1.507 1.23c.02.02.03.027.035.036l.022.042c.008.027.01.037.01.048l.064 1.45 1.7 1.423-.036-4.26zm6.3-4.507l-.36 4.153-1.2-.828.13-2.118c0-.024-.002-.033-.003-.04-.006-.032-.012-.046-.02-.06s-.02-.028-.032-.04a.23.23 0 0 0-.032-.028l-2.56-1.69.037-1.856 4.03 2.51" fill="#123d10"/><path d="M16.59 11.116l-.335-7.84-7.53 2.894 1.23 8.4 6.635-3.453zm.4 9.135l-.246-5.78-6.27 3.57.88 6.01 5.638-3.798zm.127 2.93l-5.333 3.816.648 4.422 4.872-3.88-.186-4.357zm2.465-1.762l.036 4.275 3.8-3.032.253-4.17-4.1 2.926zm9.48-6.782l-.534 3.955-2.998 2.4.352-4.068 3.18-2.276" fill="#33b652"/><path d="M17.472 22.812l-.008-.042a.21.21 0 0 0-.019-.044c-.015-.024-.023-.032-.03-.04l-1.52-1.24 1.386-.934a.19.19 0 0 0 .084-.166l-.014-.33 1.837 1.402.036 4.26-1.7-1.423-.062-1.44zm-7.398-4.772l.852 5.823-4.038-3.804-1.17-5.648 4.356 3.63zm6.904 2.212L11.34 24.05l-.88-6.01 6.27-3.57.246 5.78zm-.725-16.975l.335 7.84-6.635 3.453-1.23-8.4 7.53-2.894zM8.335 6.16l1.2 8.258-4.562-3.556-1.612-7.8L8.335 6.16zm.07 21.225l-.862-4.165L11.386 27l.615 4.203-3.596-3.817zm8.885.152l-4.872 3.88-.648-4.422 5.333-3.816.186 4.357zm6.116-4.876l-3.8 3.032-.036-4.275 4.1-2.926-.253 4.17zm.53-2.428l.13-2.118c0-.024-.002-.033-.003-.04-.006-.032-.012-.046-.02-.06s-.02-.028-.032-.04a.23.23 0 0 0-.032-.028l-2.56-1.69.037-1.856 4.03 2.51-.36 4.153-1.2-.828zm1.58.747l.352-4.068 3.18-2.276-.534 3.955-2.998 2.4zm3.97-6.77l-.006-.03c-.002-.01-.006-.02-.01-.03a.23.23 0 0 0-.027-.045c-.02-.023-.03-.03-.04-.038l-4.368-2.42c-.06-.033-.133-.032-.192.008l-3.674 2.246c-.006 0-.01.01-.016.013s-.013.01-.02.015l-.016.02c-.005.008-.01.01-.014.018s-.008.017-.01.026-.006.013-.008.02-.003.02-.004.03l-.042 1.97-1.494-.987c-.062-.04-.142-.042-.205 0l-2.15 1.314-.093-2.186-.007-.042c-.002-.008-.004-.013-.007-.02a.19.19 0 0 0-.011-.024c-.004-.008-.008-.013-.013-.02s-.01-.013-.015-.02-.012-.01-.02-.016l-2.25-1.514 2.094-1.1c.066-.034.106-.104.103-.178l-.352-8.228c-.001-.01-.003-.02-.005-.03-.006-.03-.013-.045-.022-.06s-.022-.03-.032-.04c-.017-.017-.022-.02-.028-.024-.017-.008-.02-.008-.022-.015L10.873.115a.19.19 0 0 0-.14-.011L3.036 2.502l-.05.028-.04.037c-.006.008-.01.015-.014.023s-.01.015-.013.024-.006.02-.01.03c-.006.03-.005.04-.005.05s0 .018.001.027l1.718 8.302c.01.044.034.084.07.112l2.33 1.817-1.685.802c-.02.008-.022.015-.026.016l-.027.023c-.022.024-.028.036-.034.047-.014.028-.02.045-.02.062a.24.24 0 0 0 .002.055l1.292 6.25a.19.19 0 0 0 .056.1l1.622 1.528-1.075.658c-.014.008-.026.02-.038.03-.017.02-.025.033-.032.045a.22.22 0 0 0-.021.065c-.002.018-.001.036.003.055l1 4.842c.007.034.024.066.048.092l4.048 4.298c.006.008.013.01.02.017.02.017.033.024.047.03.027.008.05.013.072.013s.038 0 .056-.01.022-.008.027-.015c.008 0 .014-.01.02-.014l5.223-4.157c.048-.04.074-.097.072-.157l-.122-2.85 1.74 1.464c.02.015.03.023.04.028s.02.008.025.015c.02.008.038.01.057.01s.037-.008.056-.01c.017-.008.022-.008.026-.015.01-.008.017-.01.026-.017l4.186-3.337c.043-.034.068-.084.072-.138l.127-2.09 1.27.884c.012.008.015.015.02.015.007.008.015.008.023.01.033.015.05.015.067.015s.038 0 .056-.01.02-.008.026-.015c.01-.008.02-.012.03-.018l3.415-2.722c.04-.03.064-.076.07-.124l.604-4.47.001-.037" fill="#231f20"/></svg>

Before

Width:  |  Height:  |  Size: 3.4 KiB

+2 -2
View File
@@ -27,7 +27,7 @@ import google from '@/assets/ico/vendor/google.svg?c';
import googlecloud from '@/assets/ico/vendor/googlecloud.svg?c';
import kubernetes from '@/assets/ico/vendor/kubernetes.svg?c';
import helm from '@/assets/ico/vendor/helm.svg?c';
import linode from '@/assets/ico/vendor/linode.svg?c';
import akamai from '@/assets/ico/vendor/akamai.svg?c';
import microsoft from '@/assets/ico/vendor/microsoft.svg?c';
import microsofticon from '@/assets/ico/vendor/microsoft-icon.svg?c';
import openldap from '@/assets/ico/vendor/openldap.svg?c';
@@ -62,7 +62,7 @@ export const SvgIcons = {
googlecloud,
kubernetes,
helm,
linode,
akamai,
microsoft,
microsofticon,
openldap,
@@ -56,9 +56,9 @@ export function KubeConfigTeaserForm() {
</li>
</ul>
<p>
Note: Officially supported cloud providers are Civo, Linode,
DigitalOcean and Microsoft Azure (others are not guaranteed to
work at present)
Note: Officially supported cloud providers are Civo, Akamai
Connected Cloud, DigitalOcean and Microsoft Azure (others are
not guaranteed to work at present)
</p>
</div>
</div>
+1 -1
View File
@@ -2,7 +2,7 @@
"author": "Portainer.io",
"name": "portainer",
"homepage": "http://portainer.io",
"version": "2.21.1",
"version": "2.21.3",
"repository": {
"type": "git",
"url": "git@github.com:portainer/portainer.git"