Skip to content

Commit

Permalink
[WIP] Adopt metal-go
Browse files Browse the repository at this point in the history
  • Loading branch information
ctreatma committed May 2, 2023
1 parent 0df28dc commit bbef589
Show file tree
Hide file tree
Showing 11 changed files with 205 additions and 176 deletions.
77 changes: 33 additions & 44 deletions cmd/ci-clean/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ limitations under the License.
package main

import (
"context"
"errors"
"fmt"
"math/rand"
"os"
"strings"
"time"

"github.com/packethost/packngo"
metal "github.com/equinix-labs/metal-go/metal/v1"
"github.com/spf13/cobra"
kerrors "k8s.io/apimachinery/pkg/util/errors"

Expand Down Expand Up @@ -55,7 +56,7 @@ func main() {
return fmt.Errorf("%s: %w", ProjectIDEnvVar, ErrMissingRequiredEnvVar)
}

return cleanup(metalAuthToken, metalProjectID) //nolint:wrapcheck
return cleanup(context.Background(), metalAuthToken, metalProjectID) //nolint:wrapcheck
},
}

Expand All @@ -64,79 +65,72 @@ func main() {
}
}

func cleanup(metalAuthToken, metalProjectID string) error {
func cleanup(ctx context.Context, metalAuthToken, metalProjectID string) error {
metalClient := packet.NewClient(metalAuthToken)
listOpts := &packngo.ListOptions{}
var errs []error

devices, _, err := metalClient.Devices.List(metalProjectID, listOpts)
devices, _, err := metalClient.DevicesApi.FindProjectDevices(ctx, metalProjectID).Execute()
if err != nil {
return fmt.Errorf("failed to list devices: %w", err)
}

if err := deleteDevices(metalClient, devices); err != nil {
if err := deleteDevices(ctx, metalClient, *devices); err != nil {
errs = append(errs, err)
}

ips, _, err := metalClient.ProjectIPs.List(metalProjectID, listOpts)
ips, _, err := metalClient.IPAddressesApi.FindIPReservations(ctx, metalProjectID).Execute()
if err != nil {
return fmt.Errorf("failed to list ip addresses: %w", err)
}

if err := deleteIPs(metalClient, ips); err != nil {
if err := deleteIPs(ctx, metalClient, *ips); err != nil {
errs = append(errs, err)
}

keys, _, err := metalClient.Projects.ListSSHKeys(metalProjectID, listOpts)
keys, _, err := metalClient.SSHKeysApi.FindProjectSSHKeys(ctx, metalProjectID).Execute()
if err != nil {
return fmt.Errorf("failed to list ssh keys: %w", err)
}

if err := deleteKeys(metalClient, keys); err != nil {
if err := deleteKeys(ctx, metalClient, *keys); err != nil {
errs = append(errs, err)
}

return kerrors.NewAggregate(errs)
}

func deleteDevices(metalClient *packet.Client, devices []packngo.Device) error {
func deleteDevices(ctx context.Context, metalClient *packet.Client, devices metal.DeviceList) error {
var errs []error

for _, d := range devices {
created, err := time.Parse(time.RFC3339, d.Created)
if err != nil {
errs = append(errs, fmt.Errorf("failed to parse creation time for device %q: %w", d.Hostname, err))
continue
}
if time.Since(created) > 4*time.Hour {
fmt.Printf("Deleting device: %s\n", d.Hostname)
_, err := metalClient.Devices.Delete(d.ID, false)
for _, d := range devices.Devices {
if time.Since(d.GetCreatedAt()) > 4*time.Hour {
hostname := d.GetHostname()
fmt.Printf("Deleting device: %s\n", hostname)
_, err := metalClient.DevicesApi.DeleteDevice(ctx, d.GetId()).ForceDelete(false).Execute()
if err != nil {
errs = append(errs, fmt.Errorf("failed to delete device %q: %w", d.Hostname, err))
errs = append(errs, fmt.Errorf("failed to delete device %q: %w", hostname, err))
}
}
}

return kerrors.NewAggregate(errs)
}

func deleteIPs(metalClient *packet.Client, ips []packngo.IPAddressReservation) error {
func deleteIPs(ctx context.Context, metalClient *packet.Client, ips metal.IPReservationList) error {
var errs []error

for _, ip := range ips {
created, err := time.Parse(time.RFC3339, ip.Created)
if err != nil {
errs = append(errs, fmt.Errorf("failed to parse creation time for ip address %q: %w", ip.Address, err))
continue
}

if time.Since(created) > 4*time.Hour {
for _, reservation := range ips.IpAddresses {
// TODO: per the spec, `reservation` could be an `IPReservation` or a `VrfIpReservation`
// maybe metal-go could define and we could move the if block to function that takes
// that interface as an argument
ip := reservation.IPReservation
if ip != nil && time.Since(ip.GetCreatedAt()) > 4*time.Hour {
for _, tag := range ip.Tags {
if strings.HasPrefix(tag, "cluster-api-provider-packet:cluster-id:") || strings.HasPrefix(tag, "usage=cloud-provider-equinix-metal-auto") {
fmt.Printf("Deleting IP: %s\n", ip.Address)
fmt.Printf("Deleting IP: %s\n", ip.GetAddress())

if _, err := metalClient.ProjectIPs.Remove(ip.ID); err != nil {
errs = append(errs, fmt.Errorf("failed to delete ip address %q: %w", ip.Address, err))
if _, err := metalClient.IPAddressesApi.DeleteIPAddress(ctx, ip.GetId()).Execute(); err != nil {
errs = append(errs, fmt.Errorf("failed to delete ip address %q: %w", ip.GetAddress(), err))
}

break
Expand All @@ -148,20 +142,15 @@ func deleteIPs(metalClient *packet.Client, ips []packngo.IPAddressReservation) e
return kerrors.NewAggregate(errs)
}

func deleteKeys(metalClient *packet.Client, keys []packngo.SSHKey) error {
func deleteKeys(ctx context.Context, metalClient *packet.Client, keys metal.SSHKeyList) error {
var errs []error

for _, k := range keys {
created, err := time.Parse(time.RFC3339, k.Created)
if err != nil {
errs = append(errs, fmt.Errorf("failed to parse creation time for SSH Key %q: %w", k.Label, err))
continue
}
if time.Since(created) > 4*time.Hour {
fmt.Printf("Deleting SSH Key: %s\n", k.Label)
_, err := metalClient.SSHKeys.Delete(k.ID)
for _, k := range keys.SshKeys {
if time.Since(k.GetCreatedAt()) > 4*time.Hour {
fmt.Printf("Deleting SSH Key: %s\n", k.GetLabel())
_, err := metalClient.SSHKeysApi.DeleteSSHKey(ctx, k.GetId()).Execute()
if err != nil {
errs = append(errs, fmt.Errorf("failed to delete SSH Key %q: %w", k.Label, err))
errs = append(errs, fmt.Errorf("failed to delete SSH Key %q: %w", k.GetLabel(), err))
}
}
}
Expand Down
8 changes: 4 additions & 4 deletions controllers/packetcluster_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func (r *PacketClusterReconciler) reconcileNormal(ctx context.Context, clusterSc

packetCluster := clusterScope.PacketCluster

ipReserv, err := r.PacketClient.GetIPByClusterIdentifier(clusterScope.Namespace(), clusterScope.Name(), packetCluster.Spec.ProjectID)
ipReserv, err := r.PacketClient.GetIPByClusterIdentifier(ctx, clusterScope.Namespace(), clusterScope.Name(), packetCluster.Spec.ProjectID)
switch {
case errors.Is(err, packet.ErrControlPlanEndpointNotFound):
// Parse metro and facility from the cluster spec
Expand All @@ -128,7 +128,7 @@ func (r *PacketClusterReconciler) reconcileNormal(ctx context.Context, clusterSc
}

// There is not an ElasticIP with the right tags, at this point we can create one
ip, err := r.PacketClient.CreateIP(clusterScope.Namespace(), clusterScope.Name(), packetCluster.Spec.ProjectID, facility, metro)
ip, err := r.PacketClient.CreateIP(ctx, clusterScope.Namespace(), clusterScope.Name(), packetCluster.Spec.ProjectID, facility, metro)
if err != nil {
log.Error(err, "error reserving an ip")
return ctrl.Result{}, err
Expand All @@ -143,13 +143,13 @@ func (r *PacketClusterReconciler) reconcileNormal(ctx context.Context, clusterSc
default:
// If there is an ElasticIP with the right tag just use it again
clusterScope.PacketCluster.Spec.ControlPlaneEndpoint = clusterv1.APIEndpoint{
Host: ipReserv.Address,
Host: ipReserv.GetAddress(),
Port: 6443,
}
}

if clusterScope.PacketCluster.Spec.VIPManager == "KUBE_VIP" {
if err := r.PacketClient.EnableProjectBGP(packetCluster.Spec.ProjectID); err != nil {
if err := r.PacketClient.EnableProjectBGP(ctx, packetCluster.Spec.ProjectID); err != nil {
log.Error(err, "error enabling bgp for project")
return ctrl.Result{}, err
}
Expand Down
68 changes: 38 additions & 30 deletions controllers/packetmachine_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import (
"strings"
"time"

"github.com/packethost/packngo"
metal "github.com/equinix-labs/metal-go/metal/v1"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
clusterv1 "sigs.k8s.io/cluster-api/api/v1beta1"
Expand Down Expand Up @@ -258,26 +258,26 @@ func (r *PacketMachineReconciler) reconcile(ctx context.Context, machineScope *s

providerID := machineScope.GetInstanceID()
var (
dev *packngo.Device
dev *metal.Device
addrs []corev1.NodeAddress
err error
controlPlaneEndpoint packngo.IPAddressReservation
controlPlaneEndpoint *metal.IPReservation
resp *http.Response
)

if providerID != "" {
// If we already have a providerID, then retrieve the device using the
// providerID. This means that the Machine has already been created
// and we successfully recorded the providerID.
dev, err = r.PacketClient.GetDevice(providerID)
dev, resp, err = r.PacketClient.GetDevice(ctx, providerID) //nolint:bodyclose
if err != nil {
var perr *packngo.ErrorResponse
if errors.As(err, &perr) && perr.Response != nil {
if perr.Response.StatusCode == http.StatusNotFound {
if resp != nil {
if resp.StatusCode == http.StatusNotFound {
machineScope.SetFailureReason(capierrors.UpdateMachineError)
machineScope.SetFailureMessage(fmt.Errorf("failed to find device: %w", err))
log.Error(err, "unable to find device")
conditions.MarkFalse(machineScope.PacketMachine, infrav1.DeviceReadyCondition, infrav1.InstanceNotFoundReason, clusterv1.ConditionSeverityError, err.Error())
} else if perr.Response.StatusCode == http.StatusForbidden {
} else if resp.StatusCode == http.StatusForbidden {
machineScope.SetFailureReason(capierrors.UpdateMachineError)
log.Error(err, "device failed to provision")
machineScope.SetFailureMessage(fmt.Errorf("device failed to provision: %w", err))
Expand All @@ -294,6 +294,7 @@ func (r *PacketMachineReconciler) reconcile(ctx context.Context, machineScope *s
// created a device by using the tags that we assign to devices
// on creation.
dev, err = r.PacketClient.GetDeviceByTags(
ctx,
machineScope.PacketCluster.Spec.ProjectID,
packet.DefaultCreateTags(machineScope.Namespace(), machineScope.Machine.Name, machineScope.Cluster.Name),
)
Expand Down Expand Up @@ -324,19 +325,20 @@ func (r *PacketMachineReconciler) reconcile(ctx context.Context, machineScope *s
// to template out the kube-vip deployment
if machineScope.IsControlPlane() {
controlPlaneEndpoint, _ = r.PacketClient.GetIPByClusterIdentifier(
ctx,
machineScope.Cluster.Namespace,
machineScope.Cluster.Name,
machineScope.PacketCluster.Spec.ProjectID)
if machineScope.PacketCluster.Spec.VIPManager == "CPEM" {
if len(controlPlaneEndpoint.Assignments) == 0 {
a := corev1.NodeAddress{
Type: corev1.NodeExternalIP,
Address: controlPlaneEndpoint.Address,
Address: controlPlaneEndpoint.GetAddress(),
}
addrs = append(addrs, a)
}
}
createDeviceReq.ControlPlaneEndpoint = controlPlaneEndpoint.Address
createDeviceReq.ControlPlaneEndpoint = controlPlaneEndpoint.GetAddress()
}

dev, err = r.PacketClient.NewDevice(ctx, createDeviceReq)
Expand All @@ -363,11 +365,11 @@ func (r *PacketMachineReconciler) reconcile(ctx context.Context, machineScope *s
}

// we do not need to set this as equinixmetal://<id> because SetProviderID() does the formatting for us
machineScope.SetProviderID(dev.ID)
machineScope.SetInstanceStatus(infrav1.PacketResourceStatus(dev.State))
machineScope.SetProviderID(dev.GetId())
machineScope.SetInstanceStatus(infrav1.PacketResourceStatus(dev.GetState()))

if machineScope.PacketCluster.Spec.VIPManager == "KUBE_VIP" {
if err := r.PacketClient.EnsureNodeBGPEnabled(dev.ID); err != nil {
if err := r.PacketClient.EnsureNodeBGPEnabled(ctx, dev.GetId()); err != nil {
// Do not treat an error enabling bgp on machine as fatal
return ctrl.Result{RequeueAfter: time.Second * 20}, fmt.Errorf("failed to enable bgp on machine %s: %w", machineScope.Name(), err)
}
Expand All @@ -379,7 +381,7 @@ func (r *PacketMachineReconciler) reconcile(ctx context.Context, machineScope *s
// Proceed to reconcile the PacketMachine state.
var result reconcile.Result

switch infrav1.PacketResourceStatus(dev.State) {
switch infrav1.PacketResourceStatus(dev.GetState()) {
case infrav1.PacketResourceStatusNew, infrav1.PacketResourceStatusQueued, infrav1.PacketResourceStatusProvisioning:
log.Info("Machine instance is pending", "instance-id", machineScope.GetInstanceID())
machineScope.SetNotReady()
Expand All @@ -389,13 +391,15 @@ func (r *PacketMachineReconciler) reconcile(ctx context.Context, machineScope *s

if machineScope.PacketCluster.Spec.VIPManager == "CPEM" {
controlPlaneEndpoint, _ = r.PacketClient.GetIPByClusterIdentifier(
ctx,
machineScope.Cluster.Namespace,
machineScope.Cluster.Name,
machineScope.PacketCluster.Spec.ProjectID)
if len(controlPlaneEndpoint.Assignments) == 0 && machineScope.IsControlPlane() {
if _, _, err := r.PacketClient.DeviceIPs.Assign(dev.ID, &packngo.AddressStruct{
Address: controlPlaneEndpoint.Address,
}); err != nil {
apiRequest := r.PacketClient.DevicesApi.CreateIPAssignment(ctx, *dev.Id).IPAssignmentInput(metal.IPAssignmentInput{
Address: controlPlaneEndpoint.GetAddress(),
})
if _, _, err := apiRequest.Execute(); err != nil { //nolint:bodyclose
log.Error(err, "err assigining elastic ip to control plane. retrying...")
return ctrl.Result{RequeueAfter: time.Second * 20}, nil
}
Expand All @@ -408,22 +412,24 @@ func (r *PacketMachineReconciler) reconcile(ctx context.Context, machineScope *s
result = ctrl.Result{}
default:
machineScope.SetNotReady()
log.Info("Equinix Metal device state is undefined", "state", dev.State, "device-id", machineScope.GetInstanceID())
log.Info("Equinix Metal device state is undefined", "state", dev.GetState(), "device-id", machineScope.GetInstanceID())
machineScope.SetFailureReason(capierrors.UpdateMachineError)
machineScope.SetFailureMessage(fmt.Errorf("instance status %q is unexpected", dev.State)) //nolint:goerr113
machineScope.SetFailureMessage(fmt.Errorf("instance status %q is unexpected", dev.GetState())) //nolint:goerr113
conditions.MarkUnknown(machineScope.PacketMachine, infrav1.DeviceReadyCondition, "", "")

result = ctrl.Result{}
}

// If Metro or Facility has changed in the spec, verify that the facility's metro is compatible with the requested spec change.
deviceFacility := dev.Facility.Code
deviceMetro := dev.Metro.Code

if machineScope.PacketMachine.Spec.Facility != "" && machineScope.PacketMachine.Spec.Facility != dev.Facility.Code {
return ctrl.Result{}, fmt.Errorf("%w: %s != %s", ErrFacilityMatch, machineScope.PacketMachine.Spec.Facility, dev.Facility.Code)
if machineScope.PacketMachine.Spec.Facility != "" && machineScope.PacketMachine.Spec.Facility != *deviceFacility {
return ctrl.Result{}, fmt.Errorf("%w: %s != %s", ErrFacilityMatch, machineScope.PacketMachine.Spec.Facility, *deviceFacility)
}

if machineScope.PacketMachine.Spec.Metro != "" && machineScope.PacketMachine.Spec.Metro != dev.Metro.Code {
return ctrl.Result{}, fmt.Errorf("%w: %s != %s", ErrMetroMatch, machineScope.PacketMachine.Spec.Facility, dev.Facility.Code)
if machineScope.PacketMachine.Spec.Metro != "" && machineScope.PacketMachine.Spec.Metro != *deviceMetro {
return ctrl.Result{}, fmt.Errorf("%w: %s != %s", ErrMetroMatch, machineScope.PacketMachine.Spec.Facility, *deviceMetro)
}

return result, nil
Expand All @@ -436,12 +442,13 @@ func (r *PacketMachineReconciler) reconcileDelete(ctx context.Context, machineSc
packetmachine := machineScope.PacketMachine
providerID := machineScope.GetInstanceID()

var device *packngo.Device
var device *metal.Device

if providerID == "" {
// If no providerID was recorded, check to see if there are any instances
// that match by tags
dev, err := r.PacketClient.GetDeviceByTags(
ctx,
machineScope.PacketCluster.Spec.ProjectID,
packet.DefaultCreateTags(machineScope.Namespace(), machineScope.Machine.Name, machineScope.Cluster.Name),
)
Expand All @@ -457,20 +464,20 @@ func (r *PacketMachineReconciler) reconcileDelete(ctx context.Context, machineSc

device = dev
} else {
var resp *http.Response
// Otherwise, try to retrieve the device by the providerID
dev, err := r.PacketClient.GetDevice(providerID)
dev, resp, err := r.PacketClient.GetDevice(ctx, providerID) //nolint:bodyclose
if err != nil {
var errResp *packngo.ErrorResponse
if errors.As(err, &errResp) && errResp.Response != nil {
if errResp.Response.StatusCode == http.StatusNotFound {
if resp != nil {
if resp.StatusCode == http.StatusNotFound {
// When the server does not exist we do not have anything left to do.
// Probably somebody manually deleted the server from the UI or via API.
log.Info("Server not found by id, nothing left to do")
controllerutil.RemoveFinalizer(packetmachine, infrav1.MachineFinalizer)
return ctrl.Result{}, nil
}

if errResp.Response.StatusCode == http.StatusForbidden {
if resp.StatusCode == http.StatusForbidden {
// When a server fails to provision it will return a 403
log.Info("Server appears to have failed provisioning, nothing left to do")
controllerutil.RemoveFinalizer(packetmachine, infrav1.MachineFinalizer)
Expand All @@ -490,7 +497,8 @@ func (r *PacketMachineReconciler) reconcileDelete(ctx context.Context, machineSc
return ctrl.Result{}, fmt.Errorf("%w: %s", ErrMissingDevice, packetmachine.Name)
}

if _, err := r.PacketClient.Devices.Delete(device.ID, force); err != nil {
apiRequest := r.PacketClient.DevicesApi.DeleteDevice(ctx, device.GetId()).ForceDelete(force)
if _, err := apiRequest.Execute(); err != nil { //nolint:bodyclose
return ctrl.Result{}, fmt.Errorf("failed to delete the machine: %w", err)
}

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ module sigs.k8s.io/cluster-api-provider-packet
go 1.19

require (
github.com/equinix-labs/metal-go v0.7.1
github.com/onsi/gomega v1.24.1
github.com/packethost/packngo v0.29.0
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.6.1
github.com/spf13/pflag v1.0.5
Expand Down
Loading

0 comments on commit bbef589

Please sign in to comment.