Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

🏃 Adopt metal-go #527

Merged
merged 2 commits into from
Aug 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 // see https://github.com/timakin/bodyclose/issues/42
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 // see https://github.com/timakin/bodyclose/issues/42
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 // see https://github.com/timakin/bodyclose/issues/42
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 // see https://github.com/timakin/bodyclose/issues/42
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.15.0
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