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

chore(code): reduce the code duplication EE-7278 (#11969)

This commit is contained in:
andres-portainer 2024-06-26 18:14:22 -03:00 committed by GitHub
parent 39bdfa4512
commit 9ee092aa5e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
85 changed files with 520 additions and 618 deletions

View file

@ -1,144 +0,0 @@
// Package concurrent provides utilities for running multiple functions concurrently in Go.
// For example, many kubernetes calls can take a while to fulfill. Oftentimes in Portainer
// we need to get a list of objects from multiple kubernetes REST APIs. We can often call these
// apis concurrently to speed up the response time.
// This package provides a clean way to do just that.
//
// Examples:
// The ConfigMaps and Secrets function converted using concurrent.Run.
/*
// GetConfigMapsAndSecrets gets all the ConfigMaps AND all the Secrets for a
// given namespace in a k8s endpoint. The result is a list of both config maps
// and secrets. The IsSecret boolean property indicates if a given struct is a
// secret or configmap.
func (kcl *KubeClient) GetConfigMapsAndSecrets(namespace string) ([]models.K8sConfigMapOrSecret, error) {
// use closures to capture the current kube client and namespace by declaring wrapper functions
// that match the interface signature for concurrent.Func
listConfigMaps := func(ctx context.Context) (interface{}, error) {
return kcl.cli.CoreV1().ConfigMaps(namespace).List(context.Background(), meta.ListOptions{})
}
listSecrets := func(ctx context.Context) (interface{}, error) {
return kcl.cli.CoreV1().Secrets(namespace).List(context.Background(), meta.ListOptions{})
}
// run the functions concurrently and wait for results. We can also pass in a context to cancel.
// e.g. Deadline timer.
results, err := concurrent.Run(context.TODO(), listConfigMaps, listSecrets)
if err != nil {
return nil, err
}
var configMapList *core.ConfigMapList
var secretList *core.SecretList
for _, r := range results {
switch v := r.Result.(type) {
case *core.ConfigMapList:
configMapList = v
case *core.SecretList:
secretList = v
}
}
// TODO: Applications
var combined []models.K8sConfigMapOrSecret
for _, m := range configMapList.Items {
var cm models.K8sConfigMapOrSecret
cm.UID = string(m.UID)
cm.Name = m.Name
cm.Namespace = m.Namespace
cm.Annotations = m.Annotations
cm.Data = m.Data
cm.CreationDate = m.CreationTimestamp.Time.UTC().Format(time.RFC3339)
combined = append(combined, cm)
}
for _, s := range secretList.Items {
var secret models.K8sConfigMapOrSecret
secret.UID = string(s.UID)
secret.Name = s.Name
secret.Namespace = s.Namespace
secret.Annotations = s.Annotations
secret.Data = msbToMss(s.Data)
secret.CreationDate = s.CreationTimestamp.Time.UTC().Format(time.RFC3339)
secret.IsSecret = true
secret.SecretType = string(s.Type)
combined = append(combined, secret)
}
return combined, nil
}
*/
package concurrent
import (
"context"
"sync"
)
// Result contains the result and any error returned from running a client task function
type Result struct {
Result any // the result of running the task function
Err error // any error that occurred while running the task function
}
// Func is a function returns a result or error
type Func func(ctx context.Context) (any, error)
// Run runs a list of functions returns the results
func Run(ctx context.Context, maxConcurrency int, tasks ...Func) ([]Result, error) {
var wg sync.WaitGroup
resultsChan := make(chan Result, len(tasks))
taskChan := make(chan Func, len(tasks))
localCtx, cancelCtx := context.WithCancel(ctx)
defer cancelCtx()
runTask := func() {
defer wg.Done()
for fn := range taskChan {
result, err := fn(localCtx)
resultsChan <- Result{Result: result, Err: err}
}
}
// Set maxConcurrency to the number of tasks if zero or negative
if maxConcurrency <= 0 {
maxConcurrency = len(tasks)
}
// Start worker goroutines
for i := 0; i < maxConcurrency; i++ {
wg.Add(1)
go runTask()
}
// Add tasks to the task channel
for _, fn := range tasks {
taskChan <- fn
}
// Close the task channel to signal workers to stop when all tasks are done
close(taskChan)
// Wait for all workers to complete
wg.Wait()
close(resultsChan)
// Collect the results and cancel on error
results := make([]Result, 0, len(tasks))
for r := range resultsChan {
if r.Err != nil {
cancelCtx()
return nil, r.Err
}
results = append(results, r)
}
return results, nil
}

View file

@ -4,7 +4,7 @@ import (
portainer "github.com/portainer/portainer/api"
"github.com/portainer/portainer/api/dataservices"
"github.com/portainer/portainer/api/internal/endpointutils"
"github.com/portainer/portainer/api/internal/tag"
"github.com/portainer/portainer/api/tag"
)
// EdgeGroupRelatedEndpoints returns a list of environments(endpoints) related to this Edge group

View file

@ -37,12 +37,12 @@ func (service *Service) BuildEdgeStack(
registries []portainer.RegistryID,
useManifestNamespaces bool,
) (*portainer.EdgeStack, error) {
err := validateUniqueName(tx.EdgeStack().EdgeStacks, name)
if err != nil {
if err := validateUniqueName(tx.EdgeStack().EdgeStacks, name); err != nil {
return nil, err
}
stackID := tx.EdgeStack().GetNextIdentifier()
return &portainer.EdgeStack{
ID: portainer.EdgeStackID(stackID),
Name: name,
@ -77,7 +77,6 @@ func (service *Service) PersistEdgeStack(
storeManifest edgetypes.StoreManifestFunc) (*portainer.EdgeStack, error) {
relationConfig, err := edge.FetchEndpointRelationsConfig(tx)
if err != nil {
return nil, fmt.Errorf("unable to find environment relations in database: %w", err)
}
@ -87,6 +86,7 @@ func (service *Service) PersistEdgeStack(
if errors.Is(err, edge.ErrEdgeGroupNotFound) {
return nil, httperrors.NewInvalidPayloadError(err.Error())
}
return nil, fmt.Errorf("unable to persist environment relation in database: %w", err)
}
@ -101,13 +101,11 @@ func (service *Service) PersistEdgeStack(
stack.EntryPoint = composePath
stack.NumDeployments = len(relatedEndpointIds)
err = service.updateEndpointRelations(tx, stack.ID, relatedEndpointIds)
if err != nil {
if err := service.updateEndpointRelations(tx, stack.ID, relatedEndpointIds); err != nil {
return nil, fmt.Errorf("unable to update endpoint relations: %w", err)
}
err = tx.EdgeStack().Create(stack.ID, stack)
if err != nil {
if err := tx.EdgeStack().Create(stack.ID, stack); err != nil {
return nil, err
}
@ -126,8 +124,7 @@ func (service *Service) updateEndpointRelations(tx dataservices.DataStoreTx, edg
relation.EdgeStacks[edgeStackID] = true
err = endpointRelationService.UpdateEndpointRelation(endpointID, relation)
if err != nil {
if err := endpointRelationService.UpdateEndpointRelation(endpointID, relation); err != nil {
return fmt.Errorf("unable to persist endpoint relation in database: %w", err)
}
}
@ -155,14 +152,12 @@ func (service *Service) DeleteEdgeStack(tx dataservices.DataStoreTx, edgeStackID
delete(relation.EdgeStacks, edgeStackID)
err = tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation)
if err != nil {
if err := tx.EndpointRelation().UpdateEndpointRelation(endpointID, relation); err != nil {
return errors.WithMessage(err, "Unable to persist environment relation in database")
}
}
err = tx.EdgeStack().DeleteEdgeStack(edgeStackID)
if err != nil {
if err := tx.EdgeStack().DeleteEdgeStack(edgeStackID); err != nil {
return errors.WithMessage(err, "Unable to remove the edge stack from the database")
}

View file

@ -4,6 +4,7 @@ import (
"testing"
portainer "github.com/portainer/portainer/api"
"github.com/stretchr/testify/assert"
)

View file

@ -1,20 +0,0 @@
package logoutcontext
import (
"context"
)
const LogoutPrefix = "logout-"
func GetContext(token string) context.Context {
return GetService(logoutToken(token)).GetLogoutCtx()
}
func Cancel(token string) {
GetService(logoutToken(token)).Cancel()
RemoveService(logoutToken(token))
}
func logoutToken(token string) string {
return LogoutPrefix + token
}

View file

@ -1,28 +0,0 @@
package logoutcontext
import (
"context"
)
type (
Service struct {
ctx context.Context
cancel context.CancelFunc
}
)
func NewService() *Service {
ctx, cancel := context.WithCancel(context.Background())
return &Service{
ctx: ctx,
cancel: cancel,
}
}
func (s *Service) Cancel() {
s.cancel()
}
func (s *Service) GetLogoutCtx() context.Context {
return s.ctx
}

View file

@ -1,34 +0,0 @@
package logoutcontext
import "sync"
type (
ServiceFactory struct {
mu sync.Mutex
services map[string]*Service
}
)
var serviceFactory = ServiceFactory{
services: make(map[string]*Service),
}
func GetService(token string) *Service {
serviceFactory.mu.Lock()
defer serviceFactory.mu.Unlock()
service, ok := serviceFactory.services[token]
if !ok {
service = NewService()
serviceFactory.services[token] = service
}
return service
}
func RemoveService(token string) {
serviceFactory.mu.Lock()
defer serviceFactory.mu.Unlock()
delete(serviceFactory.services, token)
}

View file

@ -8,6 +8,7 @@ import (
// NodesCount returns the total node number of all environments
func NodesCount(endpoints []portainer.Endpoint) int {
nodes := 0
for _, env := range endpoints {
if !endpointutils.IsEdgeEndpoint(&env) || env.UserTrusted {
nodes += countNodes(&env)
@ -28,11 +29,3 @@ func countNodes(endpoint *portainer.Endpoint) int {
return 1
}
func max(a, b int) int {
if a > b {
return a
}
return b
}

View file

@ -1,16 +0,0 @@
package securecookie
import (
"crypto/rand"
"io"
)
// GenerateRandomKey generates a random key of specified length
// source: https://github.com/gorilla/securecookie/blob/master/securecookie.go#L515
func GenerateRandomKey(length int) []byte {
k := make([]byte, length)
if _, err := io.ReadFull(rand.Reader, k); err != nil {
return nil
}
return k
}

View file

@ -1,111 +0,0 @@
package set
type SetKey interface {
~int | ~string
}
type Set[T SetKey] map[T]bool
// Add adds a key to the set.
func (s Set[T]) Add(key T) {
s[key] = true
}
// Contains returns true if the set contains the key.
func (s Set[T]) Contains(key T) bool {
_, ok := s[key]
return ok
}
// Remove removes a key from the set.
func (s Set[T]) Remove(key T) {
delete(s, key)
}
// Len returns the number of keys in the set.
func (s Set[T]) Len() int {
return len(s)
}
// IsEmpty returns true if the set is empty.
func (s Set[T]) IsEmpty() bool {
return len(s) == 0
}
// Clear removes all keys from the set.
func (s Set[T]) Keys() []T {
keys := make([]T, s.Len())
i := 0
for k := range s {
keys[i] = k
i++
}
return keys
}
// Clear removes all keys from the set.
func (s Set[T]) Copy() Set[T] {
c := make(Set[T])
for key := range s {
c.Add(key)
}
return c
}
// Difference returns a new set containing the keys that are in the first set but not in the second set.
func (set Set[T]) Difference(second Set[T]) Set[T] {
difference := set.Copy()
for key := range second {
difference.Remove(key)
}
return difference
}
// Union returns a new set containing the keys that are in either set.
func Union[T SetKey](sets ...Set[T]) Set[T] {
union := make(Set[T])
for _, set := range sets {
for key := range set {
union.Add(key)
}
}
return union
}
// Intersection returns a new set containing the keys that are in all sets.
func Intersection[T SetKey](sets ...Set[T]) Set[T] {
if len(sets) == 0 {
return make(Set[T])
}
intersection := sets[0].Copy()
for _, set := range sets[1:] {
for key := range intersection {
if !set.Contains(key) {
intersection.Remove(key)
}
}
}
return intersection
}
// ToSet returns a new set containing the keys.
func ToSet[T SetKey](keys []T) Set[T] {
set := make(Set[T])
for _, key := range keys {
set.Add(key)
}
return set
}

View file

@ -1,23 +0,0 @@
package slices
// Map applies the given function to each element of the slice and returns a new slice with the results
func Map[T, U any](s []T, f func(T) U) []U {
result := make([]U, len(s))
for i, v := range s {
result[i] = f(v)
}
return result
}
// Filter returns a new slice containing only the elements of the slice for which the given predicate returns true
func Filter[T any](s []T, predicate func(T) bool) []T {
n := 0
for _, v := range s {
if predicate(v) {
s[n] = v
n++
}
}
return s[:n]
}

View file

@ -1,127 +0,0 @@
package slices
import (
"strconv"
"testing"
"github.com/stretchr/testify/assert"
)
type filterTestCase[T any] struct {
name string
input []T
expected []T
predicate func(T) bool
}
func TestFilter(t *testing.T) {
intTestCases := []filterTestCase[int]{
{
name: "Filter even numbers",
input: []int{1, 2, 3, 4, 5, 6, 7, 8, 9},
expected: []int{2, 4, 6, 8},
predicate: func(n int) bool {
return n%2 == 0
},
},
{
name: "Filter odd numbers",
input: []int{1, 2, 3, 4, 5, 6, 7, 8, 9},
expected: []int{1, 3, 5, 7, 9},
predicate: func(n int) bool {
return n%2 != 0
},
},
}
runTestCases(t, intTestCases)
stringTestCases := []filterTestCase[string]{
{
name: "Filter strings starting with 'A'",
input: []string{"Apple", "Banana", "Avocado", "Grapes", "Apricot"},
expected: []string{"Apple", "Avocado", "Apricot"},
predicate: func(s string) bool {
return s[0] == 'A'
},
},
{
name: "Filter strings longer than 5 characters",
input: []string{"Apple", "Banana", "Avocado", "Grapes", "Apricot"},
expected: []string{"Banana", "Avocado", "Grapes", "Apricot"},
predicate: func(s string) bool {
return len(s) > 5
},
},
}
runTestCases(t, stringTestCases)
}
func runTestCases[T any](t *testing.T, testCases []filterTestCase[T]) {
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
is := assert.New(t)
result := Filter(testCase.input, testCase.predicate)
is.Equal(len(testCase.expected), len(result))
is.ElementsMatch(testCase.expected, result)
})
}
}
func TestMap(t *testing.T) {
intTestCases := []struct {
name string
input []int
expected []string
mapper func(int) string
}{
{
name: "Map integers to strings",
input: []int{1, 2, 3, 4, 5},
expected: []string{"1", "2", "3", "4", "5"},
mapper: strconv.Itoa,
},
}
runMapTestCases(t, intTestCases)
stringTestCases := []struct {
name string
input []string
expected []int
mapper func(string) int
}{
{
name: "Map strings to integers",
input: []string{"1", "2", "3", "4", "5"},
expected: []int{1, 2, 3, 4, 5},
mapper: func(s string) int {
n, _ := strconv.Atoi(s)
return n
},
},
}
runMapTestCases(t, stringTestCases)
}
func runMapTestCases[T, U any](t *testing.T, testCases []struct {
name string
input []T
expected []U
mapper func(T) U
}) {
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
is := assert.New(t)
result := Map(testCase.input, testCase.mapper)
is.Equal(len(testCase.expected), len(result))
is.ElementsMatch(testCase.expected, result)
})
}
}

View file

@ -1,76 +0,0 @@
package tag
import portainer "github.com/portainer/portainer/api"
type tagSet map[portainer.TagID]bool
// Set converts an array of ids to a set
func Set(tagIDs []portainer.TagID) tagSet {
set := map[portainer.TagID]bool{}
for _, tagID := range tagIDs {
set[tagID] = true
}
return set
}
// Intersection returns a set intersection of the provided sets
func Intersection(sets ...tagSet) tagSet {
intersection := tagSet{}
if len(sets) == 0 {
return intersection
}
setA := sets[0]
for tag := range setA {
inAll := true
for _, setB := range sets {
if !setB[tag] {
inAll = false
break
}
}
if inAll {
intersection[tag] = true
}
}
return intersection
}
// 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
}
}
return union
}
// Contains return true if setA contains setB
func Contains(setA tagSet, setB tagSet) bool {
if len(setA) == 0 || len(setB) == 0 {
return false
}
for tag := range setB {
if !setA[tag] {
return false
}
}
return true
}
// Difference returns the set difference tagsA - tagsB
func Difference(setA tagSet, setB tagSet) tagSet {
set := tagSet{}
for tag := range setA {
if !setB[tag] {
set[tag] = true
}
}
return set
}

View file

@ -1,11 +0,0 @@
package tag
// FullMatch returns true if environment tags matches all edge group tags
func FullMatch(edgeGroupTags tagSet, 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
}

View file

@ -1,135 +0,0 @@
package tag
import (
"testing"
portainer "github.com/portainer/portainer/api"
)
func TestFullMatch(t *testing.T) {
cases := []struct {
name string
edgeGroupTags tagSet
environmentTag tagSet
expected bool
}{
{
name: "environment tag partially match edge group tags",
edgeGroupTags: Set([]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}),
environmentTag: Set([]portainer.TagID{1, 2}),
expected: true,
},
{
name: "environment tags fully match edge group tags",
edgeGroupTags: Set([]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}),
environmentTag: Set([]portainer.TagID{3, 4}),
expected: false,
},
{
name: "edge group has no tags and environment has tags",
edgeGroupTags: Set([]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}),
environmentTag: Set([]portainer.TagID{}),
expected: false,
},
{
name: "both edge group and environment have no tags",
edgeGroupTags: Set([]portainer.TagID{}),
environmentTag: Set([]portainer.TagID{}),
expected: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result := FullMatch(tc.edgeGroupTags, tc.environmentTag)
if result != tc.expected {
t.Errorf("Expected %v, got %v", tc.expected, result)
}
})
}
}
func TestPartialMatch(t *testing.T) {
cases := []struct {
name string
edgeGroupTags tagSet
environmentTag tagSet
expected bool
}{
{
name: "environment tags partially match edge group tags 1",
edgeGroupTags: Set([]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}),
environmentTag: Set([]portainer.TagID{1, 4, 5}),
expected: true,
},
{
name: "edge group tags equal to environment tags",
edgeGroupTags: Set([]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}),
environmentTag: Set([]portainer.TagID{1, 2, 3}),
expected: true,
},
{
name: "environment tags do not match edge group tags",
edgeGroupTags: Set([]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{}),
environmentTag: Set([]portainer.TagID{1, 2}),
expected: false,
},
{
name: "edge group has tags and environment has no tags",
edgeGroupTags: Set([]portainer.TagID{1, 2}),
environmentTag: Set([]portainer.TagID{}),
expected: false,
},
{
name: "both edge group and environment have no tags",
edgeGroupTags: Set([]portainer.TagID{}),
environmentTag: Set([]portainer.TagID{}),
expected: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result := PartialMatch(tc.edgeGroupTags, tc.environmentTag)
if result != tc.expected {
t.Errorf("Expected %v, got %v", tc.expected, result)
}
})
}
}

View file

@ -1,204 +0,0 @@
package tag
import (
"reflect"
"testing"
portainer "github.com/portainer/portainer/api"
)
func TestIntersection(t *testing.T) {
cases := []struct {
name string
setA tagSet
setB tagSet
expected tagSet
}{
{
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}),
},
{
name: "empty setA intersection",
setA: Set([]portainer.TagID{1, 2, 3}),
setB: Set([]portainer.TagID{}),
expected: Set([]portainer.TagID{}),
},
{
name: "empty setB intersection",
setA: Set([]portainer.TagID{}),
setB: Set([]portainer.TagID{1, 2, 3}),
expected: Set([]portainer.TagID{}),
},
{
name: "no common elements sets intersection",
setA: Set([]portainer.TagID{1, 2, 3}),
setB: Set([]portainer.TagID{4, 5, 6}),
expected: Set([]portainer.TagID{}),
},
{
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}),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result := Intersection(tc.setA, tc.setB)
if !reflect.DeepEqual(result, tc.expected) {
t.Errorf("Expected %v, got %v", tc.expected, result)
}
})
}
}
func TestUnion(t *testing.T) {
cases := []struct {
name string
setA tagSet
setB tagSet
expected tagSet
}{
{
name: "non-duplicate set union",
setA: Set([]portainer.TagID{1, 2, 3}),
setB: Set([]portainer.TagID{4, 5, 6}),
expected: Set([]portainer.TagID{1, 2, 3, 4, 5, 6}),
},
{
name: "empty setA union",
setA: Set([]portainer.TagID{1, 2, 3}),
setB: Set([]portainer.TagID{}),
expected: Set([]portainer.TagID{1, 2, 3}),
},
{
name: "empty setB union",
setA: Set([]portainer.TagID{}),
setB: Set([]portainer.TagID{1, 2, 3}),
expected: Set([]portainer.TagID{1, 2, 3}),
},
{
name: "duplicate elements in set union",
setA: Set([]portainer.TagID{1, 2, 3}),
setB: Set([]portainer.TagID{3, 4, 5}),
expected: Set([]portainer.TagID{1, 2, 3, 4, 5}),
},
{
name: "equal sets union",
setA: Set([]portainer.TagID{1, 2, 3}),
setB: Set([]portainer.TagID{1, 2, 3}),
expected: Set([]portainer.TagID{1, 2, 3}),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result := Union(tc.setA, tc.setB)
if !reflect.DeepEqual(result, tc.expected) {
t.Errorf("Expected %v, got %v", tc.expected, result)
}
})
}
}
func TestContains(t *testing.T) {
cases := []struct {
name string
setA tagSet
setB tagSet
expected bool
}{
{
name: "setA contains setB",
setA: Set([]portainer.TagID{1, 2, 3}),
setB: Set([]portainer.TagID{1, 2}),
expected: true,
},
{
name: "setA equals to setB",
setA: Set([]portainer.TagID{1, 2}),
setB: Set([]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}),
expected: false,
},
{
name: "setA does not contain setB",
setA: Set([]portainer.TagID{1, 2}),
setB: Set([]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}),
expected: false,
},
{
name: "setA is not empty and setB is empty",
setA: Set([]portainer.TagID{1, 2}),
setB: Set([]portainer.TagID{}),
expected: false,
},
{
name: "setA is empty and setB is empty",
setA: Set([]portainer.TagID{}),
setB: Set([]portainer.TagID{}),
expected: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result := Contains(tc.setA, tc.setB)
if result != tc.expected {
t.Errorf("Expected %v, got %v", tc.expected, result)
}
})
}
}
func TestDifference(t *testing.T) {
cases := []struct {
name string
setA tagSet
setB tagSet
expected tagSet
}{
{
name: "positive numbers set difference",
setA: Set([]portainer.TagID{1, 2, 3, 4, 5}),
setB: Set([]portainer.TagID{4, 5, 6, 7}),
expected: Set([]portainer.TagID{1, 2, 3}),
},
{
name: "empty set difference",
setA: Set([]portainer.TagID{1, 2, 3}),
setB: Set([]portainer.TagID{}),
expected: Set([]portainer.TagID{1, 2, 3}),
},
{
name: "equal sets difference",
setA: Set([]portainer.TagID{1, 2, 3}),
setB: Set([]portainer.TagID{1, 2, 3}),
expected: Set([]portainer.TagID{}),
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
result := Difference(tc.setA, tc.setB)
if !reflect.DeepEqual(result, tc.expected) {
t.Errorf("Expected %v, got %v", tc.expected, result)
}
})
}
}

View file

@ -1,41 +0,0 @@
package unique
func Unique[T comparable](items []T) []T {
return UniqueBy(items, func(item T) T {
return item
})
}
func UniqueBy[ItemType any, ComparableType comparable](items []ItemType, accessorFunc func(ItemType) ComparableType) []ItemType {
includedItems := make(map[ComparableType]bool)
result := []ItemType{}
for _, item := range items {
if _, isIncluded := includedItems[accessorFunc(item)]; !isIncluded {
includedItems[accessorFunc(item)] = true
result = append(result, item)
}
}
return result
}
/**
type someType struct {
id int
fn func()
}
func Test() {
ids := []int{1, 2, 3, 3}
_ = UniqueBy(ids, func(id int) int { return id })
_ = Unique(ids) // shorthand for UniqueBy Identity/self
as := []someType{{id: 1}, {id: 2}, {id: 3}, {id: 3}}
_ = UniqueBy(as, func(item someType) int { return item.id }) // no error
_ = UniqueBy(as, func(item someType) someType { return item }) // compile error - someType is not comparable
_ = Unique(as) // compile error - shorthand fails for the same reason
}
*/

View file

@ -1,22 +0,0 @@
package url
import (
"net/url"
"strings"
)
// ParseURL parses the endpointURL using url.Parse.
//
// to prevent an error when url has port but no protocol prefix
// we add `//` prefix if needed
func ParseURL(endpointURL string) (*url.URL, error) {
if !strings.HasPrefix(endpointURL, "http") &&
!strings.HasPrefix(endpointURL, "tcp") &&
!strings.HasPrefix(endpointURL, "//") &&
!strings.HasPrefix(endpointURL, `unix:`) &&
!strings.HasPrefix(endpointURL, `npipe:`) {
endpointURL = "//" + endpointURL
}
return url.Parse(endpointURL)
}