diff --git a/metal/cloud_test.go b/metal/cloud_test.go index 6179afb..d64b7a0 100644 --- a/metal/cloud_test.go +++ b/metal/cloud_test.go @@ -1,13 +1,16 @@ package metal import ( + "encoding/json" "fmt" + "net/http" "net/http/httptest" "net/url" "testing" metal "github.com/equinix/equinix-sdk-go/services/metalv1" "github.com/google/uuid" + "github.com/gorilla/mux" clientset "k8s.io/client-go/kubernetes" k8sfake "k8s.io/client-go/kubernetes/fake" restclient "k8s.io/client-go/rest" @@ -23,6 +26,16 @@ const ( validZoneCode = "ewr1" ) +type MockMetalServer struct { + DeviceStore map[string]*metal.Device + ProjectStore map[string]struct { + Devices []*metal.Device + BgpEnabled bool + } + + T *testing.T +} + // mockControllerClientBuilder mock implementation of https://pkg.go.dev/k8s.io/cloud-provider#ControllerClientBuilder // so we can pass it to cloud.Initialize() type mockControllerClientBuilder struct{} @@ -44,9 +57,16 @@ func (m mockControllerClientBuilder) ClientOrDie(name string) clientset.Interfac } // create a valid cloud with a client -func testGetValidCloud(t *testing.T, LoadBalancerSetting string) *cloud { +func testGetValidCloud(t *testing.T, LoadBalancerSetting string) (*cloud, *MockMetalServer) { + mockServer := &MockMetalServer{ + DeviceStore: map[string]*metal.Device{}, + ProjectStore: map[string]struct { + Devices []*metal.Device + BgpEnabled bool + }{}, + } // mock endpoint so our client can handle it - ts := httptest.NewServer(nil) + ts := httptest.NewServer(mockServer.CreateHandler()) url, _ := url.Parse(ts.URL) urlString := url.String() @@ -62,11 +82,11 @@ func testGetValidCloud(t *testing.T, LoadBalancerSetting string) *cloud { ccb := &mockControllerClientBuilder{} c.Initialize(ccb, nil) - return c.(*cloud) + return c.(*cloud), mockServer } func TestLoadBalancerDefaultDisabled(t *testing.T) { - vc := testGetValidCloud(t, "") + vc, _ := testGetValidCloud(t, "") response, supported := vc.LoadBalancer() var ( expectedSupported = false @@ -82,7 +102,7 @@ func TestLoadBalancerDefaultDisabled(t *testing.T) { func TestLoadBalancerMetalLB(t *testing.T) { t.Skip("Test needs a k8s client to work") - vc := testGetValidCloud(t, "metallb:///metallb-system/config") + vc, _ := testGetValidCloud(t, "metallb:///metallb-system/config") response, supported := vc.LoadBalancer() var ( expectedSupported = true @@ -98,7 +118,7 @@ func TestLoadBalancerMetalLB(t *testing.T) { func TestLoadBalancerEmpty(t *testing.T) { t.Skip("Test needs a k8s client to work") - vc := testGetValidCloud(t, "empty://") + vc, _ := testGetValidCloud(t, "empty://") response, supported := vc.LoadBalancer() var ( expectedSupported = true @@ -114,7 +134,7 @@ func TestLoadBalancerEmpty(t *testing.T) { func TestLoadBalancerKubeVIP(t *testing.T) { t.Skip("Test needs a k8s client to work") - vc := testGetValidCloud(t, "kube-vip://") + vc, _ := testGetValidCloud(t, "kube-vip://") response, supported := vc.LoadBalancer() var ( expectedSupported = true @@ -129,7 +149,7 @@ func TestLoadBalancerKubeVIP(t *testing.T) { } func TestInstances(t *testing.T) { - vc := testGetValidCloud(t, "") + vc, _ := testGetValidCloud(t, "") response, supported := vc.Instances() expectedSupported := false expectedResponse := cloudprovider.Instances(nil) @@ -142,7 +162,7 @@ func TestInstances(t *testing.T) { } func TestClusters(t *testing.T) { - vc := testGetValidCloud(t, "") + vc, _ := testGetValidCloud(t, "") response, supported := vc.Clusters() var ( expectedSupported = false @@ -157,7 +177,7 @@ func TestClusters(t *testing.T) { } func TestRoutes(t *testing.T) { - vc := testGetValidCloud(t, "") + vc, _ := testGetValidCloud(t, "") response, supported := vc.Routes() var ( expectedSupported = false @@ -172,7 +192,7 @@ func TestRoutes(t *testing.T) { } func TestProviderName(t *testing.T) { - vc := testGetValidCloud(t, "") + vc, _ := testGetValidCloud(t, "") name := vc.ProviderName() if name != ProviderName { t.Errorf("returned %s instead of expected %s", name, ProviderName) @@ -180,7 +200,7 @@ func TestProviderName(t *testing.T) { } func TestHasClusterID(t *testing.T) { - vc := testGetValidCloud(t, "") + vc, _ := testGetValidCloud(t, "") cid := vc.HasClusterID() expectedCid := true if cid != expectedCid { @@ -218,3 +238,60 @@ func constructClient(authToken string, baseUrl *string) *metal.APIClient { configuration.UserAgent = fmt.Sprintf("cloud-provider-equinix-metal/%s %s", version.Get(), configuration.UserAgent) return metal.NewAPIClient(configuration) } + +func (s *MockMetalServer) CreateHandler() http.Handler { + r := mux.NewRouter() + // create a BGP config for a project + r.HandleFunc("/projects/{projectID}/bgp-configs", s.createBGPHandler).Methods("POST") + // get all devices for a project + r.HandleFunc("/projects/{projectID}/devices", s.listDevicesHandler).Methods("GET") + // get a single device + r.HandleFunc("/devices/{deviceID}", s.getDeviceHandler).Methods("GET") + // handle metadata requests + return r +} + +func (s *MockMetalServer) listDevicesHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + projectID := vars["projectID"] + + data := s.ProjectStore[projectID] + devices := data.Devices + var resp = struct { + Devices []*metal.Device `json:"devices"` + }{ + Devices: devices, + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(&resp); err != nil { + s.T.Fatal(err.Error()) + } +} + +// get information about a specific device +func (s *MockMetalServer) getDeviceHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + volID := vars["deviceID"] + dev := s.DeviceStore[volID] + w.Header().Set("Content-Type", "application/json") + if dev != nil { + err := json.NewEncoder(w).Encode(dev) + if err != nil { + s.T.Fatal(err) + } + return + } + w.WriteHeader(http.StatusNotFound) +} + +// createBGPHandler enable BGP for a project +func (s *MockMetalServer) createBGPHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + projectID := vars["projectID"] + projectData := s.ProjectStore[projectID] + projectData.BgpEnabled = true + s.ProjectStore[projectID] = projectData + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) +} diff --git a/metal/devices.go b/metal/devices.go index 0e94274..32ec659 100644 --- a/metal/devices.go +++ b/metal/devices.go @@ -83,9 +83,6 @@ func (i *instances) InstanceMetadata(ctx context.Context, node *v1.Node) (*cloud // // https://kubernetes.io/docs/reference/labels-annotations-taints/#topologykubernetesiozone - if device.Facility != nil { - z = device.Facility.GetCode() - } if device.Metro != nil { r = device.Metro.GetCode() } @@ -158,8 +155,8 @@ func (i *instances) deviceByNode(node *v1.Node) (*metal.Device, error) { func deviceByID(client *metal.DevicesApiService, id string) (*metal.Device, error) { klog.V(2).Infof("called deviceByID with ID %s", id) - device, _, err := client.FindDeviceById(context.Background(), id).Execute() - if isNotFound(err) { + device, resp, err := client.FindDeviceById(context.Background(), id).Execute() + if isNotFound(resp, err) { return nil, cloudprovider.InstanceNotFound } return device, err diff --git a/metal/devices_test.go b/metal/devices_test.go index 106bfd4..3a3ffd7 100644 --- a/metal/devices_test.go +++ b/metal/devices_test.go @@ -34,23 +34,26 @@ func testNode(providerID, nodeName string) *v1.Node { } func TestNodeAddresses(t *testing.T) { - vc := testGetValidCloud(t, "") + vc, server := testGetValidCloud(t, "") inst, _ := vc.InstancesV2() if inst == nil { t.Fatal("inst is nil") } devName := testGetNewDevName() - //facility, _ := testGetOrCreateValidZone(validZoneName, validZoneCode, backend) - //plan, _ := testGetOrCreateValidPlan(validPlanName, validPlanSlug, backend) - //dev, _ := backend.CreateDevice(projectID, devName, plan, facility) uid := uuid.New().String() state := metal.DEVICESTATE_ACTIVE + dev := &metal.Device{ Id: &uid, Hostname: &devName, State: &state, Plan: metal.NewPlan(), } + server.DeviceStore[uid] = dev + project := server.ProjectStore[vc.config.ProjectID] + project.Devices = append(project.Devices, dev) + server.ProjectStore[vc.config.ProjectID] = project + // update the addresses on the device; normally created by Equinix Metal itself networks := []metal.IPAssignment{ testCreateAddress(false, false), // private ipv4 @@ -81,7 +84,6 @@ func TestNodeAddresses(t *testing.T) { }{ {"empty node name", testNode("", ""), nil, fmt.Errorf("node name cannot be empty")}, {"instance not found", testNode("", nodeName), nil, fmt.Errorf("instance not found")}, - {"invalid id", testNode("equinixmetal://123", nodeName), nil, fmt.Errorf("123 is not a valid UUID")}, {"unknown name", testNode("equinixmetal://"+randomID, nodeName), nil, fmt.Errorf("instance not found")}, {"valid both", testNode("equinixmetal://"+dev.GetId(), devName), validAddresses, nil}, {"valid provider id", testNode("equinixmetal://"+dev.GetId(), nodeName), validAddresses, nil}, @@ -108,12 +110,9 @@ func TestNodeAddresses(t *testing.T) { } func TestNodeAddressesByProviderID(t *testing.T) { - vc := testGetValidCloud(t, "") + vc, server := testGetValidCloud(t, "") inst, _ := vc.InstancesV2() devName := testGetNewDevName() - //facility, _ := testGetOrCreateValidZone(validZoneName, validZoneCode, backend) - //plan, _ := testGetOrCreateValidPlan(validPlanName, validPlanSlug, backend) - //dev, _ := backend.CreateDevice(projectID, devName, plan, facility) uid := uuid.New().String() state := metal.DEVICESTATE_ACTIVE dev := &metal.Device{ @@ -123,6 +122,11 @@ func TestNodeAddressesByProviderID(t *testing.T) { Plan: metal.NewPlan(), } + server.DeviceStore[uid] = dev + project := server.ProjectStore[vc.config.ProjectID] + project.Devices = append(project.Devices, dev) + server.ProjectStore[vc.config.ProjectID] = project + // update the addresses on the device; normally created by Equinix Metal itself networks := []metal.IPAssignment{ testCreateAddress(false, false), // private ipv4 @@ -172,7 +176,7 @@ func TestNodeAddressesByProviderID(t *testing.T) { /* func TestInstanceID(t *testing.T) { - vc := testGetValidCloud(t, "") + vc, server := testGetValidCloud(t, "") inst, _ := vc.InstancesV2() devName := testGetNewDevName() facility, _ := testGetOrCreateValidZone(validZoneName, validZoneCode, backend) @@ -206,9 +210,10 @@ func TestNodeAddressesByProviderID(t *testing.T) { } */ func TestInstanceType(t *testing.T) { - vc := testGetValidCloud(t, "") + vc, server := testGetValidCloud(t, "") inst, _ := vc.InstancesV2() devName := testGetNewDevName() + uid := uuid.New().String() state := metal.DEVICESTATE_ACTIVE dev := &metal.Device{ @@ -217,6 +222,11 @@ func TestInstanceType(t *testing.T) { State: &state, Plan: metal.NewPlan(), } + server.DeviceStore[uid] = dev + project := server.ProjectStore[vc.config.ProjectID] + project.Devices = append(project.Devices, dev) + server.ProjectStore[vc.config.ProjectID] = project + privateIP := "10.1.1.2" publicIP := "25.50.75.100" trueBool := true @@ -241,7 +251,6 @@ func TestInstanceType(t *testing.T) { err error }{ {"empty name", "", "", fmt.Errorf("instance not found")}, - {"invalid id", "thisdoesnotexist", "", fmt.Errorf("thisdoesnotexist is not a valid UUID")}, {"unknown name", randomID, "", fmt.Errorf("instance not found")}, {"valid", "equinixmetal://" + dev.GetId(), dev.Plan.GetSlug(), nil}, } @@ -264,7 +273,7 @@ func TestInstanceType(t *testing.T) { } func TestInstanceZone(t *testing.T) { - vc := testGetValidCloud(t, "") + vc, server := testGetValidCloud(t, "") inst, _ := vc.InstancesV2() devName := testGetNewDevName() uid := uuid.New().String() @@ -275,6 +284,7 @@ func TestInstanceZone(t *testing.T) { State: &state, Plan: metal.NewPlan(), } + privateIP := "10.1.1.2" publicIP := "25.50.75.100" @@ -300,32 +310,32 @@ func TestInstanceZone(t *testing.T) { }, }...) + server.DeviceStore[uid] = dev + project := server.ProjectStore[vc.config.ProjectID] + project.Devices = append(project.Devices, dev) + server.ProjectStore[vc.config.ProjectID] = project + tests := []struct { testName string name string region string - zone string err error }{ - {"empty name", "", "", "", fmt.Errorf("instance not found")}, - {"invalid id", "thisdoesnotexist", "", "", fmt.Errorf("thisdoesnotexist is not a valid UUID")}, - {"unknown name", randomID, "", "", fmt.Errorf("instance not found")}, - {"valid", "equinixmetal://" + dev.GetId(), validRegionCode, validZoneCode, nil}, + {"empty name", "", "", fmt.Errorf("instance not found")}, + {"unknown name", randomID, "", fmt.Errorf("instance not found")}, + {"valid", "equinixmetal://" + dev.GetId(), validRegionCode, nil}, } for i, tt := range tests { t.Run(tt.testName, func(t *testing.T) { - var zone, region string + var region string md, err := inst.InstanceMetadata(context.TODO(), testNode(tt.name, nodeName)) if md != nil { - zone = md.Zone region = md.Region } switch { case (err == nil && tt.err != nil) || (err != nil && tt.err == nil) || (err != nil && tt.err != nil && !strings.HasPrefix(err.Error(), tt.err.Error())): t.Errorf("%d: mismatched errors, actual %v expected %v", i, err, tt.err) - case zone != tt.zone: - t.Errorf("%d: mismatched zone, actual %v expected %v", i, zone, tt.zone) case region != tt.region: t.Errorf("%d: mismatched region, actual %v expected %v", i, region, tt.region) } @@ -335,7 +345,7 @@ func TestInstanceZone(t *testing.T) { /* func TestInstanceTypeByProviderID(t *testing.T) { - vc := testGetValidCloud(t, "") + vc, server := testGetValidCloud(t, "") inst, _ := vc.Instances() devName := testGetNewDevName() uid := uuid.New().String() @@ -411,7 +421,7 @@ func TestCurrentNodeName(t *testing.T) { */ func TestInstanceExistsByProviderID(t *testing.T) { - vc := testGetValidCloud(t, "") + vc, server := testGetValidCloud(t, "") inst, _ := vc.InstancesV2() devName := testGetNewDevName() uid := uuid.New().String() @@ -423,6 +433,11 @@ func TestInstanceExistsByProviderID(t *testing.T) { Plan: metal.NewPlan(), } + server.DeviceStore[uid] = dev + project := server.ProjectStore[vc.config.ProjectID] + project.Devices = append(project.Devices, dev) + server.ProjectStore[vc.config.ProjectID] = project + tests := []struct { id string exists bool @@ -449,26 +464,34 @@ func TestInstanceExistsByProviderID(t *testing.T) { } func TestInstanceShutdownByProviderID(t *testing.T) { - vc := testGetValidCloud(t, "") + vc, server := testGetValidCloud(t, "") inst, _ := vc.InstancesV2() devName := testGetNewDevName() - uid := uuid.New().String() + activeDevUid := uuid.New().String() activeState := metal.DEVICESTATE_ACTIVE devActive := &metal.Device{ - Id: &uid, + Id: &activeDevUid, Hostname: &devName, State: &activeState, Plan: metal.NewPlan(), } + server.DeviceStore[activeDevUid] = devActive + project := server.ProjectStore[vc.config.ProjectID] + project.Devices = append(project.Devices, devActive) + server.ProjectStore[vc.config.ProjectID] = project - uid = uuid.New().String() + inactiveDevUid := uuid.New().String() inactiveState := metal.DEVICESTATE_INACTIVE devInactive := &metal.Device{ - Id: &uid, + Id: &inactiveDevUid, Hostname: &devName, State: &inactiveState, Plan: metal.NewPlan(), } + server.DeviceStore[inactiveDevUid] = devInactive + project = server.ProjectStore[vc.config.ProjectID] + project.Devices = append(project.Devices, devInactive) + server.ProjectStore[vc.config.ProjectID] = project tests := []struct { id string diff --git a/metal/errors.go b/metal/errors.go index 5b31ec1..27283f7 100644 --- a/metal/errors.go +++ b/metal/errors.go @@ -1,19 +1,17 @@ package metal import ( - "github.com/packethost/packngo" + "net/http" + "strings" ) // isNotFound check if an error is a 404 not found -func isNotFound(err error) bool { +func isNotFound(resp *http.Response, err error) bool { if err == nil { return false } - if perr, ok := err.(*packngo.ErrorResponse); ok { - if perr.Response == nil { - return false - } - return perr.Response.StatusCode == 404 + if resp.StatusCode == http.StatusNotFound || strings.Contains(err.Error(), "Not Found") { + return true } return false }