Skip to content

Commit

Permalink
Add hardware normalization decorator (#2389)
Browse files Browse the repository at this point in the history
When reading hardware data for Tinkerbell we need to apply
normalizations so we can perform comparison operations. MAC Addresses
are a culprit for incorrect comparisons because comparators don't take
into consideration case insensitivty of a MAC.

We're lower-casing MAC as it seems to be lower-case more often than not.
  • Loading branch information
chrisdoherty4 authored Jun 13, 2022
1 parent d450708 commit dc30f16
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 14 deletions.
2 changes: 1 addition & 1 deletion pkg/providers/tinkerbell/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ func (p *Provider) readCSVToCatalogue() error {
// Translate all Machine instances from the p.machines source into Kubernetes object types.
// The PostBootstrapSetup() call invoked elsewhere in the program serializes the catalogue
// and submits it to the clsuter.
machines, err := hardware.NewCSVReaderFromFile(p.hardwareCSVFile)
machines, err := hardware.NewNormalizedCSVReaderFromFile(p.hardwareCSVFile)
if err != nil {
return err
}
Expand Down
26 changes: 16 additions & 10 deletions pkg/providers/tinkerbell/hardware/csv.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,6 @@ func NewCSVReader(r io.Reader) (CSVReader, error) {
return CSVReader{reader: reader}, nil
}

// NewCSVReaderFromFile creates a CSVReader instance that reads from path.
func NewCSVReaderFromFile(path string) (CSVReader, error) {
fh, err := os.Open(path)
if err != nil {
return CSVReader{}, err
}

return NewCSVReader(bufio.NewReader(fh))
}

// Read reads a single entry from the CSV data source and returns a new Machine representation.
func (cr CSVReader) Read() (Machine, error) {
machine, err := cr.reader.Read()
Expand All @@ -46,3 +36,19 @@ func (cr CSVReader) Read() (Machine, error) {
}
return machine.(Machine), nil
}

// NewNormalizedCSVReaderFromFile creates a MachineReader instance backed by a CSVReader reading from path
// that applies default normalizations to machines.
func NewNormalizedCSVReaderFromFile(path string) (MachineReader, error) {
fh, err := os.Open(path)
if err != nil {
return CSVReader{}, err
}

reader, err := NewCSVReader(bufio.NewReader(fh))
if err != nil {
return nil, err
}

return NewNormalizer(reader), nil
}
4 changes: 2 additions & 2 deletions pkg/providers/tinkerbell/hardware/csv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func TestCSVReaderWithMultipleLabels(t *testing.T) {
func TestCSVReaderFromFile(t *testing.T) {
g := gomega.NewWithT(t)

reader, err := hardware.NewCSVReaderFromFile("./testdata/hardware.csv")
reader, err := hardware.NewNormalizedCSVReaderFromFile("./testdata/hardware.csv")
g.Expect(err).ToNot(gomega.HaveOccurred())

machine, err := reader.Read()
Expand Down Expand Up @@ -89,7 +89,7 @@ func TestNewCSVReaderWithIOReaderError(t *testing.T) {
func TestCSVReaderWithoutBMCHeaders(t *testing.T) {
g := gomega.NewWithT(t)

reader, err := hardware.NewCSVReaderFromFile("./testdata/hardware_no_bmc_headers.csv")
reader, err := hardware.NewNormalizedCSVReaderFromFile("./testdata/hardware_no_bmc_headers.csv")
g.Expect(err).ToNot(gomega.HaveOccurred())

machine, err := reader.Read()
Expand Down
62 changes: 62 additions & 0 deletions pkg/providers/tinkerbell/hardware/normalize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package hardware

import "strings"

// NormalizerFunc applies a normalization transformation to the Machine.
type NormalizerFunc func(Machine) Machine

// Normalizer is a decorator for a MachineReader that applies a set of normalization funcs
// to machines.
type Normalizer struct {
reader MachineReader
normalizers []NormalizerFunc
}

// NewNormalizer creates a Normalizer instance that decorates r's Read(). A set of default
// normalization functions are pre-registered.
func NewNormalizer(r MachineReader) *Normalizer {
normalizer := NewRawNormalizer(r)
RegisterDefaultNormalizations(normalizer)
return normalizer
}

// NewRawNormalizer returns a Normalizer with default normalizations registered by
// RegisterDefaultNormalizations.
func NewRawNormalizer(r MachineReader) *Normalizer {
return &Normalizer{reader: r}
}

// Read reads an Machine from the decorated MachineReader, applies all normalization funcs and
// returns the machine. If the decorated MachineReader errors, it is returned.
func (n Normalizer) Read() (Machine, error) {
machine, err := n.reader.Read()
if err != nil {
return Machine{}, err
}

for _, fn := range n.normalizers {
machine = fn(machine)
}

return machine, nil
}

// Register fn to n such that fn is run over each machine read from the wrapped MachineReader.
func (n *Normalizer) Register(fn NormalizerFunc) {
n.normalizers = append(n.normalizers, fn)
}

// LowercaseMACAddress ensures m's MACAddress field has lower chase characters.
func LowercaseMACAddress(m Machine) Machine {
m.MACAddress = strings.ToLower(m.MACAddress)
return m
}

// RegisterDefaultNormalizations registers a set of default normalizations on n.
func RegisterDefaultNormalizations(n *Normalizer) {
for _, fn := range []NormalizerFunc{
LowercaseMACAddress,
} {
n.Register(fn)
}
}
65 changes: 65 additions & 0 deletions pkg/providers/tinkerbell/hardware/normalize_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package hardware_test

import (
"errors"
"testing"

"github.com/golang/mock/gomock"
"github.com/onsi/gomega"

"github.com/aws/eks-anywhere/pkg/providers/tinkerbell/hardware"
"github.com/aws/eks-anywhere/pkg/providers/tinkerbell/hardware/mocks"
)

func TestNormalizer(t *testing.T) {
g := gomega.NewWithT(t)
ctrl := gomock.NewController(t)
reader := mocks.NewMockMachineReader(ctrl)

normalizer := hardware.NewNormalizer(reader)

expect := NewValidMachine()
expect.MACAddress = "AA:BB:CC:DD:EE:FF"
reader.EXPECT().Read().Return(expect, (error)(nil))

machine, err := normalizer.Read()

g.Expect(err).ToNot(gomega.HaveOccurred())

// Re-use the expect machine instance and lower-case the MAC.
expect.MACAddress = "aa:bb:cc:dd:ee:ff"

g.Expect(machine).To(gomega.Equal(expect))
}

func TestRawNormalizer(t *testing.T) {
g := gomega.NewWithT(t)
ctrl := gomock.NewController(t)
reader := mocks.NewMockMachineReader(ctrl)

normalizer := hardware.NewNormalizer(reader)

expect := NewValidMachine()
expect.MACAddress = "AA:BB:CC:DD:EE:FF"
reader.EXPECT().Read().Return(expect, (error)(nil))

machine, err := normalizer.Read()

g.Expect(err).ToNot(gomega.HaveOccurred())
g.Expect(machine).To(gomega.Equal(machine))
}

func TestRawNormalizerReadError(t *testing.T) {
g := gomega.NewWithT(t)
ctrl := gomock.NewController(t)
reader := mocks.NewMockMachineReader(ctrl)

normalizer := hardware.NewNormalizer(reader)

expect := errors.New("foo bar")
reader.EXPECT().Read().Return(hardware.Machine{}, expect)

_, err := normalizer.Read()

g.Expect(err).To(gomega.HaveOccurred())
}
2 changes: 1 addition & 1 deletion pkg/providers/tinkerbell/upgrade.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func (p *Provider) SetupAndValidateUpgradeCluster(ctx context.Context, cluster *
if p.hardareCSVIsProvided() {
machineCatalogueWriter := hardware.NewMachineCatalogueWriter(p.catalogue)

machines, err := hardware.NewCSVReaderFromFile(p.hardwareCSVFile)
machines, err := hardware.NewNormalizedCSVReaderFromFile(p.hardwareCSVFile)
if err != nil {
return err
}
Expand Down

0 comments on commit dc30f16

Please sign in to comment.