From d5d8b465ff433a054d319c5f8fa4605a4b72bd9e Mon Sep 17 00:00:00 2001 From: Alva8756 Date: Sun, 9 Feb 2025 20:18:33 -0800 Subject: [PATCH] support comparing components between FleetDB and EMAPI --- README.md | 1 + cmd/get/component_gaps.go | 180 ++++++++++++++++++ cmd/get/components_type_convert.go | 293 +++++++++++++++++++++++++++++ go.mod | 5 + go.sum | 13 ++ 5 files changed, 492 insertions(+) create mode 100644 cmd/get/component_gaps.go create mode 100644 cmd/get/components_type_convert.go diff --git a/README.md b/README.md index 10bc3300..d511afa1 100644 --- a/README.md +++ b/README.md @@ -18,3 +18,4 @@ For the updated list of all commands available, check out the [CLI docs](https:/ - Retrieve information about a firmware - `mctl get firmware --id <>` - Install a firmware set on a server - `mctl install firmware-set --server <>` - Import firmware, firmware-set from file - `mctl create firmware-set --from-file samples/fw-set.json`, where the JSON file contents is the output of `mctl list firmware-set` +- Get component gaps between EMAPI and FleetDB for a server - `./mctl get component_gaps -s <>`. You will need to build the `mctl` with a build tag `-tags staff`. diff --git a/cmd/get/component_gaps.go b/cmd/get/component_gaps.go new file mode 100644 index 00000000..5abcc5d9 --- /dev/null +++ b/cmd/get/component_gaps.go @@ -0,0 +1,180 @@ +//go:build staff +// +build staff + +// We use build tag to avoid mctl infrastructure changes as staff is a private repo +// which lint and gendoc cannot access. +// To download the staff module, please run 2 commands below: +// export GOPRIVATE=go.equinixmetal.net/* +// git config --global url.ssh://git@github.com/equinixmetal.insteadOf https://github.com/equinixmetal +// See https://github.com/equinixmetal/go-staff +package get + +import ( + "context" + "fmt" + "log" + "os" + "reflect" + "sort" + + "github.com/google/uuid" + "github.com/spf13/cobra" + "go.equinixmetal.net/staff" + + mctl "github.com/metal-toolbox/mctl/cmd" + "github.com/metal-toolbox/mctl/internal/app" +) + +type getComponentGapsFlags struct { + id string +} + +var ( + flagsDefinedGetComponentGaps *getComponentGapsFlags +) + +// CompareComponents compares two objects and prints differences, even if they're not directly structs +func CompareComponents(obj1, obj2 interface{}, path string) { + if reflect.DeepEqual(obj1, obj2) { + return + } + + v1 := reflect.ValueOf(obj1) + v2 := reflect.ValueOf(obj2) + + if v1.Kind() == reflect.Ptr { + v1 = v1.Elem() + } + if v2.Kind() == reflect.Ptr { + v2 = v2.Elem() + } + + if v1.Kind() != reflect.Struct && v1.Kind() != reflect.Slice { + if v1.Interface() != v2.Interface() { + fmt.Printf("Component %s: %v != %v\n", path, v1.Interface(), v2.Interface()) + } + return + } + + typeOfObj := v1.Type() + for i := 0; i < v1.NumField(); i++ { + fieldName := typeOfObj.Field(i).Name + value1 := v1.Field(i) + value2 := v2.Field(i) + + // Build the current path for output (Component + Field) + currentPath := path + "." + fieldName + if value1.Kind() == reflect.Slice { + compareSlices(currentPath, value1, value2, typeOfObj) + } else if value1.Kind() == reflect.Struct { + CompareComponents(value1.Interface(), value2.Interface(), currentPath) + } else { + if value1.Interface() != value2.Interface() { + fmt.Printf("Component %s Field %s: %v != %v\n", path, fieldName, value1.Interface(), value2.Interface()) + } + } + } +} + +func compareSlices(path string, slice1, slice2 reflect.Value, typeOfObj reflect.Type) { + if slice1.Len() != slice2.Len() { + fmt.Printf("Component %s Field %s: lengths differ (%d != %d)\n", path, typeOfObj, slice1.Len(), slice2.Len()) + } + + // Slice item order may be different in fleetDB and EMAPI, eg {CPU1, CPU2} and {CPU2, CPU1}. + // Sort the slices by "Serial" field + sort.SliceStable(slice1.Interface(), func(i, j int) bool { + return slice1.Index(i).FieldByName("Serial").String() < slice1.Index(j).FieldByName("Serial").String() + }) + sort.SliceStable(slice2.Interface(), func(i, j int) bool { + return slice2.Index(i).FieldByName("Serial").String() < slice2.Index(j).FieldByName("Serial").String() + }) + + extraElem := "EMAPI EXTRA" + if slice1.Len() > slice2.Len() { + extraElem = "FleetDB EXTRA" + temp := slice2 + slice2 = slice1 + slice1 = temp + } + + for i := 0; i < slice1.Len(); i++ { + elem1 := slice1.Index(i) + elem2 := slice2.Index(i) + + if elem1.Kind() == reflect.Struct { + CompareComponents(elem1.Interface(), elem2.Interface(), fmt.Sprintf("%s[%d]", path, i)) + } else { + if elem1.Interface() != elem2.Interface() { + fmt.Printf("Component %s Field %s[%d]: %v != %v\n", path, typeOfObj, i, elem1.Interface(), elem2.Interface()) + } + } + } + + for i := slice1.Len(); i < slice2.Len(); i++ { + elem := slice2.Index(i) + fmt.Printf("Component %s [%v] Field %s[%d]: %v\n", path, extraElem, typeOfObj, i, elem.Interface()) + } +} + +// Get firmware info +var getComponentGaps = &cobra.Command{ + Use: "component_gaps", + Short: "Get gaps between FleetDB inventory and EMAPI hardware", + Run: func(cmd *cobra.Command, _ []string) { + theApp := mctl.MustCreateApp(cmd.Context()) + + ctx, cancel := context.WithTimeout(cmd.Context(), mctl.CmdTimeout) + defer cancel() + + fleetClient, err := app.NewFleetDBAPIClient(cmd.Context(), theApp.Config.FleetDBAPI, theApp.Reauth) + if err != nil { + log.Fatal(err) + } + + serverID, err := uuid.Parse(flagsDefinedGetComponentGaps.id) + if err != nil { + log.Fatal(err) + } + + fleetInventory, _, err := fleetClient.GetServerInventory(ctx, serverID, false) + if err != nil { + log.Fatal("fleetdb API client returned error: ", err) + } + + emapiClient, err := staff.NewClient() + if err != nil { + log.Fatal(err) + } + + opts := &staff.ListOptions{ + Excludes: []string{"firmware_version"}, + } + emapiHardware, _, err := emapiClient.Staff.Components.ListByHardwareID(serverID.String(), opts) + if err != nil { + log.Fatal(err) + } + + fleetComponents, err := convertFleetDBServer(fleetInventory) + if err != nil { + log.Fatal(err) + } + + emapiComponents, err := convertEMAPIServer(emapiHardware) + if err != nil { + log.Fatal(err) + } + + CompareComponents(fleetComponents, emapiComponents, "Component") + + os.Exit(0) + }, +} + +func init() { + cmdGet.AddCommand(getComponentGaps) + + flagsDefinedGetComponentGaps = &getComponentGapsFlags{} + mctl.AddServerFlag(getComponentGaps, &flagsDefinedGetComponentGaps.id) + mctl.RequireFlag(getComponentGaps, mctl.ServerFlag) +} diff --git a/cmd/get/components_type_convert.go b/cmd/get/components_type_convert.go new file mode 100644 index 00000000..933ba98b --- /dev/null +++ b/cmd/get/components_type_convert.go @@ -0,0 +1,293 @@ +//go:build staff +// +build staff + +package get + +import ( + "fmt" + "math" + "regexp" + "strconv" + "strings" + + "github.com/bmc-toolbox/common" + "github.com/go-playground/validator" + "github.com/metal-toolbox/rivets/v2/types" + "go.equinixmetal.net/staff" +) + +type commonComponents struct { + Chassis chassis `json:"chassis" validate:"required"` +} + +type chassis struct { + BMC BMC `json:"bmc" validate:"required"` + Motherboard Motherboard `json:"motherboard" validate:"required"` + CPU []CPU `json:"cpu" validate:"required"` + Memory []Memory `json:"memory" validate:"required"` + Disk []Disk `json:"disk" validate:"required"` + NIC []NIC `json:"nic" validate:"required"` +} + +type BMC struct { + Manufacturer string `json:"manufacturer" validate:"required"` + Model string `json:"model" validate:"required"` + Firmware Firmware `json:"firmware" validate:"required"` +} + +type Motherboard struct { + Manufacturer string `json:"manufacturer" validate:"required"` + Model string `json:"model" validate:"required"` + Firmware Firmware `json:"firmware" validate:"required"` + Serial string `json:"serial"` +} + +type CPU struct { + Manufacturer string `json:"manufacturer" validate:"required"` + Model string `json:"model" validate:"required"` + Cores int `json:"cores" validate:"required"` + ClockSpeed int64 `json:"clock_speed" validate:"required"` + Firmware Firmware `json:"firmware" validate:"required"` +} + +type Memory struct { + Manufacturer string `json:"manufacturer" validate:"required"` + Model string `json:"model" validate:"required"` + Capacity int64 `json:"capacity" validate:"required"` + Speed int64 `json:"speed" validate:"required"` + Firmware Firmware `json:"firmware" validate:"required"` + Serial string `json:"serial"` +} + +type Disk struct { + Manufacturer string `json:"manufacturer" validate:"required"` + Model string `json:"model" validate:"required"` + Capacity int64 `json:"capacity" validate:"required"` + Firmware Firmware `json:"firmware" validate:"required"` + Serial string `json:"serial"` +} + +type NIC struct { + Manufacturer string `json:"manufacturer" validate:"required"` + Model string `json:"model" validate:"required"` + Speed int64 `json:"speed" validate:"required"` + Firmware Firmware `json:"firmware" validate:"required"` + MACs []string `json:"macs" validate:"required"` +} + +type Firmware struct { + Version string `json:"version" validate:"required"` +} + +const ( + BMCType = "ManagementControllerComponent" + MotherboardType = "MotherboardComponent" + CPUType = "ProcessorComponent" + MemoryType = "MemoryComponent" + DiskType = "DiskComponent" + NICType = "NetworkComponent" + + NoFirmware = "N/A" +) + +func convertFleetDBServer(server *types.Server) (*commonComponents, error) { + commonData := &commonComponents{} + + for _, component := range server.Components { + firmware := Firmware{} + if component.Firmware != nil { + firmware = Firmware{component.Firmware.Installed} + } + attribute := component.Attributes + if attribute == nil { + attribute = &types.ComponentAttributes{} + } + + switch component.Name { + case common.SlugPhysicalMem: + commonData.Chassis.Memory = append(commonData.Chassis.Memory, Memory{ + Manufacturer: strings.ToLower(component.Vendor), + Model: attribute.PartNumber, + Speed: attribute.ClockSpeedHz, + Capacity: attribute.SizeBytes, + Firmware: firmware, + Serial: component.Serial, + }) + + case common.SlugCPU: + commonData.Chassis.CPU = append(commonData.Chassis.CPU, CPU{ + Manufacturer: strings.ToLower(component.Vendor), + Model: component.Model, + Cores: attribute.Cores, + ClockSpeed: attribute.ClockSpeedHz, + Firmware: firmware, + }) + + case common.SlugNIC: + commonData.Chassis.NIC = append(commonData.Chassis.NIC, NIC{ + Manufacturer: strings.ToLower(component.Vendor), + Model: component.Model, + // TO BE FILLED. It's is inside the `data`(json) in attribute where GetInventory hasn't parsed + // or copied to the response. + Speed: -1, + Firmware: firmware, + // TO BE FILLED. It's is inside the `data`(json) in attribute where GetInventory hasn't parsed + // or copied to the response. + MACs: []string{}, + }) + + case common.SlugBMC: + commonData.Chassis.BMC = BMC{ + Manufacturer: strings.ToLower(component.Vendor), + Model: component.Model, + Firmware: firmware, + } + + case common.SlugDrive: + var bytes int64 = attribute.CapacityBytes + + commonData.Chassis.Disk = append(commonData.Chassis.Disk, Disk{ + Manufacturer: strings.ToLower(component.Vendor), + Model: component.Model, + // Sometimes GetInventory will drop this stat while it is in the database. + Capacity: bytes, + Firmware: firmware, + Serial: component.Serial, + }) + + case common.SlugBIOS: + commonData.Chassis.Motherboard = Motherboard{ + Manufacturer: strings.ToLower(component.Vendor), + Model: component.Model, + Firmware: firmware, + Serial: component.Serial, + } + } + } + + validate := validator.New() + err := validate.Struct(commonData) + if err != nil { + fmt.Printf("[ERROR] EMAPI is missing required fields: %v\n", err) + } + + return commonData, nil +} + +func convertEMAPIServer(comps []staff.Component) (*commonComponents, error) { + commonData := &commonComponents{} + + for _, component := range comps { + firmware := Firmware{component.FirmwareVersion} + switch component.Type { + case BMCType: + commonData.Chassis.BMC = BMC{ + Manufacturer: strings.ToLower(component.Vendor), + Model: component.Model, + Firmware: firmware, + } + + case MotherboardType: + commonData.Chassis.Motherboard = Motherboard{ + Manufacturer: strings.ToLower(component.Vendor), + Model: component.Model, + Firmware: firmware, + Serial: component.Serial, + } + + case CPUType: + cores, _ := component.Data["cores"].(float64) + clock, _ := component.Data["clock"].(float64) + commonData.Chassis.CPU = append(commonData.Chassis.CPU, CPU{ + Manufacturer: strings.ToLower(component.Vendor), + Model: component.Model, + Cores: int(cores), + ClockSpeed: int64(clock), + Firmware: firmware, + }) + + case DiskType: + bytesStr := component.Data["size"] + bytes, _ := parseSizeWithUnit(bytesStr.(string)) + commonData.Chassis.Disk = append(commonData.Chassis.Disk, Disk{ + Manufacturer: strings.ToLower(component.Vendor), + Model: component.Model, + Capacity: bytes, + Firmware: firmware, + Serial: component.Serial, + }) + + case MemoryType: + capStr := component.Data["size"].(string) + cap, _ := parseSizeWithUnit(capStr) + commonData.Chassis.Memory = append(commonData.Chassis.Memory, Memory{ + Manufacturer: strings.ToLower(component.Vendor), + Model: component.Model, + // GetServerComponent and GetInventory don't have this stats currently. + Capacity: cap, + Speed: int64(math.Round(component.Data["clock"].(float64) * 1000000)), + Firmware: firmware, + Serial: component.Serial, + }) + + case NICType: + var rate int64 + if component.Data["rate"] != nil { + if rateStr, ok := component.Data["rate"].(string); ok { + rate, _ = strconv.ParseInt(rateStr, 10, 64) + } + } + + commonData.Chassis.NIC = append(commonData.Chassis.NIC, NIC{ + Manufacturer: strings.ToLower(component.Vendor), + Model: component.Model, + Speed: rate, + Firmware: firmware, + MACs: []string{component.Serial}, + }) + } + } + + validate := validator.New() + err := validate.Struct(commonData) + if err != nil { + fmt.Printf("[ERROR] EMAPI is missing required fields: %v\n", err) + } + return commonData, nil +} + +func parseSizeWithUnit(size string) (int64, error) { + if size == "" { + return 0, nil + } + + unitMap := map[string]uint64{ + "": 1, + "B": 1, + "K": 1024, + "KB": 1024, + "M": 1024 * 1024, + "MB": 1024 * 1024, + "G": 1024 * 1024 * 1024, + "GB": 1024 * 1024 * 1024, + "T": 1024 * 1024 * 1024 * 1024, + "TB": 1024 * 1024 * 1024 * 1024, + } + + size = strings.ToUpper(size) + re := regexp.MustCompile("[A-Za-z]+") + unit := re.FindString(size) + valueStr := size[:len(size)-2] + + value, err := strconv.ParseFloat(valueStr, 64) + if err != nil { + return 0, err + } + + if multiplier, ok := unitMap[unit]; ok { + bytes := int64(value * float64(multiplier)) + return bytes, nil + } + + return 0, fmt.Errorf("invalid unit: %s", unit) +} diff --git a/go.mod b/go.mod index caf91bca..369a6afe 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,12 @@ toolchain go1.23.1 require ( github.com/adrg/xdg v0.5.3 + github.com/bmc-toolbox/common v0.0.0-20240723142833-87832458b53b github.com/coreos/go-oidc/v3 v3.12.0 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/dustin/go-humanize v1.0.1 github.com/go-jose/go-jose/v4 v4.0.4 + github.com/go-playground/validator v9.31.0+incompatible github.com/google/uuid v1.6.0 github.com/metal-toolbox/bmc-common v1.0.3 github.com/metal-toolbox/bomservice v0.2.0 @@ -24,6 +26,7 @@ require ( github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.10.0 github.com/zalando/go-keyring v0.2.6 + go.equinixmetal.net/staff v0.42.0 golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 golang.org/x/net v0.34.0 golang.org/x/oauth2 v0.25.0 @@ -93,6 +96,7 @@ require ( github.com/nats-io/nats.go v1.38.0 // indirect github.com/nats-io/nkeys v0.4.9 // indirect github.com/nats-io/nuid v1.0.1 // indirect + github.com/packethost/packngo v0.30.0 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pquerna/cachecontrol v0.2.0 // indirect @@ -141,6 +145,7 @@ require ( google.golang.org/grpc v1.69.4 // indirect google.golang.org/protobuf v1.36.2 // indirect gopkg.in/go-jose/go-jose.v2 v2.6.3 // indirect + gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 3d913450..20210de8 100644 --- a/go.sum +++ b/go.sum @@ -109,6 +109,8 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce 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/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bmc-toolbox/common v0.0.0-20240723142833-87832458b53b h1:0LHjikaGWlqEMczrCEZ6w1N/ZqcYlx6WRHkhabRUQEk= +github.com/bmc-toolbox/common v0.0.0-20240723142833-87832458b53b/go.mod h1:Cdnkm+edb6C0pVkyCrwh3JTXAe0iUF9diDG/DztPI9I= github.com/bytedance/sonic v1.12.7 h1:CQU8pxOy9HToxhndH0Kx/S1qU/CuS9GnKYrGioDcU1Q= github.com/bytedance/sonic v1.12.7/go.mod h1:tnbal4mxOMju17EGfknm2XyYcpyCnIROYOEYuemj13I= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= @@ -162,6 +164,7 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -237,6 +240,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp4Mit+3FDh548oRqwVgNsHA= +github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig= github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -613,6 +618,8 @@ github.com/nirasan/go-oauth-pkce-code-verifier v0.0.0-20220510032225-4f9f17eaec4 github.com/nirasan/go-oauth-pkce-code-verifier v0.0.0-20220510032225-4f9f17eaec4c/go.mod h1:DvuJJ/w1Y59rG8UTDxsMk5U+UJXJwuvUgbiJSm9yhX8= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/packethost/packngo v0.30.0 h1:JVeTwbXXETsLTDQncUbYwIFpkOp/xevXrffM2HrFECI= +github.com/packethost/packngo v0.30.0/go.mod h1:BT/XcdwLVmeMtGPbovnxCpnI1s9ylSE1cs/7pq007NE= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= @@ -735,6 +742,7 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -784,6 +792,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= +go.equinixmetal.net/staff v0.42.0 h1:VHKk2HvsewcJWRTiWSaeACvD3f/yDmfLC3o/m+B569g= +go.equinixmetal.net/staff v0.42.0/go.mod h1:rtCgo5t+yDlUUPN0dWQnXV2lxD4zsWSaH/Ux/bQjCbs= go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= @@ -851,6 +861,7 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200420201142-3c4aac89819a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= @@ -1380,6 +1391,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/go-jose/go-jose.v2 v2.6.3 h1:nt80fvSDlhKWQgSWyHyy5CfmlQr+asih51R8PTWNKKs= gopkg.in/go-jose/go-jose.v2 v2.6.3/go.mod h1:zzZDPkNNw/c9IE7Z9jr11mBZQhKQTMzoEEIoEdZlFBI= +gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= +gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=