compare

package
v0.7.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 6, 2024 License: Apache-2.0 Imports: 5 Imported by: 2

Documentation

Overview

Package compare gives some utilities to compare Objects with little code required and to reconcile a list of desired Objects with a list of existing ones to lists of Objects to create and destroy.

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrInvalidType is returned when the given data type cannot be used.
	ErrInvalidType = errors.New("invalid type given")

	// ErrDifferentTypes is returned by functions given multiple inputs when those inputs are of different types.
	ErrDifferentTypes = fmt.Errorf("%w: cannot compare structs of different types", ErrInvalidType)

	// ErrKeyNotFound is returned when a named field is not found in a struct.
	ErrKeyNotFound = fmt.Errorf("key not found in struct")
)

Functions

func Reconcile

func Reconcile(target, existing interface{}, create, destroy *[]types.Object, compareAttributes ...string) error

Reconcile fills the given arrays of Objects to create and destroy, by adding the Objects in target but not in existing to create and adding the Objects in existing but not in target to destroy. Objects are compared with the Compare function in this package, using the provided compareAttributes.

Objects in target with a matching Object in existing are updated, filling e.g. the identifiers in target.

Example
package main

import (
	"fmt"

	"go.anx.io/go-anxcloud/pkg/api/types"
	lbaasv1 "go.anx.io/go-anxcloud/pkg/apis/lbaas/v1"

	. "github.com/onsi/ginkgo/v2"
	. "github.com/onsi/gomega"
)

func main() {
	lb := lbaasv1.LoadBalancer{Identifier: "LoadBalancer identifier"}

	// array of objects that should exist, created by business logic
	targetObjects := []lbaasv1.Frontend{
		{
			Name:         "Frontend A",
			Mode:         lbaasv1.TCP,
			LoadBalancer: &lb,
		},
		{
			Name:         "Frontend B",
			Mode:         lbaasv1.TCP,
			LoadBalancer: &lb,
		},
	}

	// array of objects found in the Engine as currently existing
	existingObjects := []lbaasv1.Frontend{
		{
			Name:         "Frontend A",
			Identifier:   "Frontend identifier A",
			Mode:         lbaasv1.TCP,
			LoadBalancer: &lb,
		},
		{
			Name:         "Frontend C",
			Identifier:   "Frontend identifier C",
			Mode:         lbaasv1.TCP,
			LoadBalancer: &lb,
		},
	}

	// those will receive the objects to create and destroy
	var toCreate []types.Object
	var toDestroy []types.Object

	err := Reconcile(
		// array of objects we want in the end and we currently have
		targetObjects, existingObjects,
		// output arrays of objects to create and destroy to change reality into our desired state
		&toCreate, &toDestroy,
		// attributes to compare
		"Name", "Mode", "LoadBalancer.Identifier",
	)
	if err != nil {
		fmt.Printf("Error reconciling objects: %v\n", err)
		return
	}

	fmt.Printf("Found %v Objects to create and %v Objects to destroy\n", len(toCreate), len(toDestroy))

	for _, c := range toCreate {
		fmt.Printf("Going to create Object named %q\n", c.(*lbaasv1.Frontend).Name)
	}

	for _, c := range toDestroy {
		fmt.Printf("Going to destroy Object named %q\n", c.(*lbaasv1.Frontend).Name)
	}

	for _, f := range targetObjects {
		if f.Identifier != "" {
			fmt.Printf("Identifier of Object named %q was retrieved: %q\n", f.Name, f.Identifier)
		}
	}

}

var _ = Describe("Reconcile", func() {
	DescribeTable("errors out for invalid input",
		func(expectedError error, target, existing interface{}, compareAttributes ...string) {
			err := Reconcile(target, existing, nil, nil, compareAttributes...)
			Expect(err).To(MatchError(expectedError))
		},
		Entry("existing not an array", ErrInvalidType, []lbaasv1.Frontend{}, lbaasv1.Frontend{}),
		Entry("target not an array", ErrInvalidType, lbaasv1.Frontend{}, []lbaasv1.Frontend{}),

		// these errors are only checked when there are entries in the target and existing arrays, I think that's ok
		// -- Mara @LittleFox94 Grosch, 2022-03-28
		Entry("invalid attribute", ErrKeyNotFound, []lbaasv1.Frontend{{}}, []lbaasv1.Frontend{{}}, "Test"),
	)

	DescribeTable("works with target being array to ...",
		func(target interface{}, expected []types.Object) {
			var actual []types.Object
			err := Reconcile(target, []types.Object{}, &actual, nil)
			Expect(err).NotTo(HaveOccurred())
			Expect(actual).To(ContainElements(expected))
		},
		Entry("structs",
			[]lbaasv1.Frontend{
				{Name: "foo"},
				{Name: "bar"},
			},
			[]types.Object{
				&lbaasv1.Frontend{Name: "foo"},
				&lbaasv1.Frontend{Name: "bar"},
			},
		),
		Entry("pointers to structs",
			[]*lbaasv1.Frontend{
				{Name: "foo"},
				{Name: "bar"},
			},
			[]types.Object{
				&lbaasv1.Frontend{Name: "foo"},
				&lbaasv1.Frontend{Name: "bar"},
			},
		),
		Entry("objects",
			[]types.Object{
				&lbaasv1.Frontend{Name: "foo"},
				&lbaasv1.Frontend{Name: "bar"},
			},
			[]types.Object{
				&lbaasv1.Frontend{Name: "foo"},
				&lbaasv1.Frontend{Name: "bar"},
			},
		),
	)
})
Output:

Found 1 Objects to create and 1 Objects to destroy
Going to create Object named "Frontend B"
Going to destroy Object named "Frontend C"
Identifier of Object named "Frontend A" was retrieved: "Frontend identifier A"
func Search(needle, haystack interface{}, compareAttributes ...string) (int, error)

Search iterates through the array haystack until it finds an Object matching needle by the given (nested) attribute names (using the Compare function in this package).

Types

type Difference

type Difference struct {
	Key string
	A   interface{}
	B   interface{}
}

Difference gives the (nested) attribute name and the values of the attribute of the two objects differing.

func Compare

func Compare(a, b interface{}, attributes ...string) ([]Difference, error)

Compare compares two objects by the given (dot-nested) attribute names.

Example
lb := lbaasv1.LoadBalancer{Identifier: "some identifier string"}
a := lbaasv1.Frontend{
	Name:         "Frontend A",
	Mode:         lbaasv1.TCP,
	LoadBalancer: &lb,
}

b := lbaasv1.Frontend{
	Name:         "Frontend B",
	Mode:         lbaasv1.TCP,
	LoadBalancer: &lb,
}

differences, err := Compare(a, b, "Name", "Mode", "LoadBalancer.Identifier")
if err != nil {
	fmt.Printf("Error comparing the objects: %v\n", err)
} else if len(differences) > 0 {
	for _, difference := range differences {
		fmt.Printf("Difference between objects on attribute %q: %q != %q\n", difference.Key, difference.A, difference.B)
	}
} else {
	fmt.Printf("Found no difference between A and B\n")
}
Output:

Difference between objects on attribute "Name": "Frontend A" != "Frontend B"
Example (NestedNil)
lb := lbaasv1.LoadBalancer{Identifier: "some identifier string"}
a := lbaasv1.Frontend{
	Name: "Frontend A",
	Mode: lbaasv1.TCP,
}

b := lbaasv1.Frontend{
	Name:         "Frontend B",
	Mode:         lbaasv1.TCP,
	LoadBalancer: &lb,
}

differences, err := Compare(a, b, "Name", "Mode", "LoadBalancer.Identifier")
if err != nil {
	fmt.Printf("Error comparing the objects: %v\n", err)
} else if len(differences) > 0 {
	for _, difference := range differences {
		fmt.Printf("Difference between objects on attribute %q: '%v' != '%v'\n", difference.Key, difference.A, difference.B)
	}
} else {
	fmt.Printf("Found no difference between A and B\n")
}
Output:

Difference between objects on attribute "Name": 'Frontend A' != 'Frontend B'
Difference between objects on attribute "LoadBalancer.Identifier": '<nil>' != 'some identifier string'

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL