Skip to content

Commit

Permalink
chore: Updated metal-go client for sub-commands hardware reservations (
Browse files Browse the repository at this point in the history
…#288)

Breakout from #270

What this PR does / why we need it:

This PR replaces packngo with metal-go for all interactions with the
Equinix Metal API specifically for hardware sub commands

DISCUSSION POINTS:

Fixes #46

Co-authored-by: codinja1188 <[email protected]>
  • Loading branch information
codinja1188 and codinja1188 authored Jan 16, 2024
1 parent 01f781b commit a2319b7
Show file tree
Hide file tree
Showing 4 changed files with 161 additions and 81 deletions.
32 changes: 7 additions & 25 deletions internal/hardware/hardware.go
Original file line number Diff line number Diff line change
@@ -1,34 +1,14 @@
// Copyright © 2018 Jasmin Gacic <[email protected]>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package hardware

import (
metal "github.com/equinix/equinix-sdk-go/services/metalv1"
"github.com/equinix/metal-cli/internal/outputs"
"github.com/packethost/packngo"
"github.com/spf13/cobra"
)

type Client struct {
Servicer Servicer
Service packngo.HardwareReservationService
Service metal.HardwareReservationsApiService
Out outputs.Outputer
}

Expand All @@ -44,7 +24,7 @@ func (c *Client) NewCommand() *cobra.Command {
root.PersistentPreRun(cmd, args)
}
}
c.Service = c.Servicer.API(cmd).HardwareReservations
c.Service = *c.Servicer.MetalAPI(cmd).HardwareReservationsApi
},
}

Expand All @@ -56,9 +36,11 @@ func (c *Client) NewCommand() *cobra.Command {
}

type Servicer interface {
API(*cobra.Command) *packngo.Client
ListOptions(defaultIncludes, defaultExcludes []string) *packngo.ListOptions
MetalAPI(*cobra.Command) *metal.APIClient
Format() outputs.Format
Filters() map[string]string
Includes(defaultIncludes []string) (incl []string)
Excludes(defaultExcludes []string) (excl []string)
}

func NewClient(s Servicer, out outputs.Outputer) *Client {
Expand Down
30 changes: 7 additions & 23 deletions internal/hardware/move.go
Original file line number Diff line number Diff line change
@@ -1,28 +1,10 @@
// Copyright © 2018 Jasmin Gacic <[email protected]>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package hardware

import (
"context"
"fmt"

metal "github.com/equinix/equinix-sdk-go/services/metalv1"
"github.com/spf13/cobra"
)

Expand All @@ -39,14 +21,16 @@ func (c *Client) Move() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
cmd.SilenceUsage = true
header := []string{"ID", "Facility", "Plan", "Created"}
r, _, err := c.Service.Move(hardwareReservationID, projectID)
moveHardReserveRequest := metal.NewMoveHardwareReservationRequest()
moveHardReserveRequest.ProjectId = &projectID
r, _, err := c.Service.MoveHardwareReservation(context.Background(), hardwareReservationID).MoveHardwareReservationRequest(*moveHardReserveRequest).Execute()
if err != nil {
return fmt.Errorf("Could not move Hardware Reservation: %w", err)
return fmt.Errorf("could not move Hardware Reservation: %w", err)
}

data := make([][]string, 1)

data[0] = []string{r.ID, r.Facility.Code, r.Plan.Name, r.CreatedAt.String()}
data[0] = []string{r.GetId(), r.Facility.GetCode(), r.Plan.GetName(), r.CreatedAt.String()}

return c.Out.Output(r, header, &data)
},
Expand Down
61 changes: 28 additions & 33 deletions internal/hardware/retrieve.go
Original file line number Diff line number Diff line change
@@ -1,30 +1,11 @@
// Copyright © 2018 Jasmin Gacic <[email protected]>
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package hardware

import (
"context"
"fmt"

metal "github.com/equinix/equinix-sdk-go/services/metalv1"
"github.com/equinix/metal-cli/internal/outputs"
"github.com/packethost/packngo"
"github.com/spf13/cobra"
)

Expand All @@ -47,6 +28,7 @@ func (c *Client) Retrieve() *cobra.Command {
header := []string{"ID", "Facility", "Metro", "Plan", "Created"}

inc := []string{}
exc := []string{}

// only fetch extra details when rendered
switch c.Servicer.Format() {
Expand All @@ -56,44 +38,57 @@ func (c *Client) Retrieve() *cobra.Command {
inc = []string{"facility.metro"}
}

listOpt := c.Servicer.ListOptions(inc, nil)

if hardwareReservationID == "" && projectID == "" {
return fmt.Errorf("either id or project-id should be set")
}

cmd.SilenceUsage = true
if hardwareReservationID != "" {
getOpts := &packngo.GetOptions{Includes: listOpt.Includes, Excludes: listOpt.Excludes}
r, _, err := c.Service.Get(hardwareReservationID, getOpts)
r, _, err := c.Service.FindHardwareReservationById(context.Background(), hardwareReservationID).Include(c.Servicer.Includes(inc)).Exclude(c.Servicer.Excludes(exc)).Execute()
if err != nil {
return fmt.Errorf("Could not get Hardware Reservation: %w", err)
return fmt.Errorf("could not get Hardware Reservation: %w", err)
}

data := make([][]string, 1)
metro := ""
if r.Facility.Metro != nil {
metro = r.Facility.Metro.Code
metro = *r.Facility.Metro.Code
}

data[0] = []string{r.ID, r.Facility.Code, metro, r.Plan.Name, r.CreatedAt.String()}
data[0] = []string{r.GetId(), r.Facility.GetCode(), metro, r.Plan.GetName(), r.CreatedAt.String()}

return c.Out.Output(r, header, &data)
}
request := c.Service.FindProjectHardwareReservations(context.Background(), projectID).Include(c.Servicer.Includes(inc)).Exclude(c.Servicer.Excludes(exc))
filters := c.Servicer.Filters()

reservations, _, err := c.Service.List(projectID, listOpt)
if err != nil {
return fmt.Errorf("Could not list Hardware Reservations: %w", err)
if filters["query"] != "" {
request = request.Query(filters["query"])
}

if filters["state"] != "" {
state, _ := metal.NewFindProjectHardwareReservationsStateParameterFromValue(filters["state"])
request = request.State(*state)
}

if filters["provisionable"] != "" {
provisionable, _ := metal.NewFindProjectHardwareReservationsProvisionableParameterFromValue(filters["provisionable"])
request = request.Provisionable(*provisionable)
}

reservationsList, err := request.ExecuteWithPagination()
if err != nil {
return fmt.Errorf("could not list Hardware Reservations: %w", err)
}
reservations := reservationsList.GetHardwareReservations()
data := make([][]string, len(reservations))

for i, r := range reservations {
metro := ""
if r.Facility.Metro != nil {
metro = r.Facility.Metro.Code
metro = r.Facility.Metro.GetCode()
}
data[i] = []string{r.ID, r.Facility.Code, metro, r.Plan.Name, r.CreatedAt.String()}
data[i] = []string{r.GetId(), r.Facility.GetCode(), metro, r.Plan.GetName(), r.CreatedAt.String()}
}

return c.Out.Output(reservations, header, &data)
Expand Down
119 changes: 119 additions & 0 deletions test/e2e/hardwaretest/hardware_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package hardwaretest

import (
"io"
"log"
"net/http"
"net/http/httptest"
"os"
"strings"
"testing"

root "github.com/equinix/metal-cli/internal/cli"
"github.com/equinix/metal-cli/internal/hardware"
outputPkg "github.com/equinix/metal-cli/internal/outputs"
"github.com/equinix/metal-cli/test/helper"
"github.com/spf13/cobra"
)

func MockRootClient(responseBody string) *root.Client {
mockAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)

_, err := w.Write([]byte(responseBody))
if err != nil {
log.Fatalf("Failed to write mock response: %v", err)
}
}))
mockClient := root.NewClient("", mockAPI.URL, "metal")
return mockClient

}

func TestCli_Hardware(t *testing.T) {
subCommand := "hardware-reservation"
// Adjust this response as needed for your tests.
mockResponse := `{
"hardware_reservations": [
{
"created_at": "2019-08-24T14:15:22Z",
"custom_rate": 1050.5,
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"facility": {
"code": "da",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f01",
"metro": {
"code": "da",
"country": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f02",
"name": "string"
},
"name": "string"
},
"plan": {
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f03",
"name": "m3.large.x86",
"slug": "m3.large.x86"
},
"project": {
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f04"
},
"provisionable": true,
"short_id": "string",
"spare": true,
"switch_uuid": "string",
"termination_time": "2019-08-24T14:15:22Z"
}
]
}`

rootClient := MockRootClient(mockResponse)

type fields struct {
MainCmd *cobra.Command
Outputer outputPkg.Outputer
}
tests := []struct {
name string
fields fields
want *cobra.Command
cmdFunc func(*testing.T, *cobra.Command)
}{
{
name: "get",
fields: fields{
MainCmd: hardware.NewClient(rootClient, outputPkg.Outputer(&outputPkg.Standard{})).NewCommand(),
Outputer: outputPkg.Outputer(&outputPkg.Standard{}),
},
want: &cobra.Command{},
cmdFunc: func(t *testing.T, c *cobra.Command) {
root := c.Root()
projectName := "metal-cli-" + helper.GenerateRandomString(5) + "-hardware-test"
project := helper.CreateTestProject(t, projectName)
root.SetArgs([]string{subCommand, "get", "-p", project.GetId()})
rescueStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
if err := root.Execute(); err != nil {
t.Error(err)
}
w.Close()
out, _ := io.ReadAll(r)

os.Stdout = rescueStdout
if !strings.Contains(string(out[:]), "da") &&
!strings.Contains(string(out[:]), "m3.large.x86") {
t.Error("expected output should include m3.large.x86.")
}
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
rootCmd := rootClient.NewCommand()
rootCmd.AddCommand(tt.fields.MainCmd)
tt.cmdFunc(t, tt.fields.MainCmd)
})
}
}

0 comments on commit a2319b7

Please sign in to comment.