| package wrapngo |
| |
| import ( |
| "context" |
| "crypto/ed25519" |
| "crypto/rand" |
| "errors" |
| "fmt" |
| "log" |
| "os" |
| "testing" |
| "time" |
| |
| "github.com/packethost/packngo" |
| "golang.org/x/crypto/ssh" |
| ) |
| |
| var ( |
| ctx context.Context |
| |
| // apiuser and apikey are the Equinix credentials necessary to test the |
| // client package. |
| |
| apiuser string = os.Getenv("EQUINIX_USER") |
| apikey string = os.Getenv("EQUINIX_APIKEY") |
| |
| // apipid references the Equinix Metal project used. It's recommended to use |
| // non-production projects in the context of testing. |
| apipid string = os.Getenv("EQUINIX_PROJECT_ID") |
| // apios specifies the operating system installed on newly provisioned |
| // devices. See Equinix Metal API documentation for details. |
| apios string = os.Getenv("EQUINIX_DEVICE_OS") |
| |
| // sshKeyID identifies the SSH public key registered with Equinix. |
| sshKeyID string |
| // sshKeyLabel is the label used to register the SSH key with Equinix. |
| sshKeyLabel string = "shepherd-client-testkey" |
| |
| // testDevice is the device created in TestCreateDevice, and later |
| // referenced to exercise implementation operating on Equinix Metal device |
| // objects. |
| testDevice *packngo.Device |
| // testDeviceHostname is the hostname used to register and reference the |
| // test device. |
| testDeviceHostname string = "shepherd-client-testdev" |
| ) |
| |
| // ensureParams returns false if any of the required environment variable |
| // parameters are missing. |
| func ensureParams() bool { |
| if apiuser == "" { |
| log.Print("EQUINIX_USER must be set.") |
| return false |
| } |
| if apikey == "" { |
| log.Print("EQUINIX_APIKEY must be set.") |
| return false |
| } |
| if apipid == "" { |
| log.Print("EQUINIX_PROJECT_ID must be set.") |
| return false |
| } |
| if apios == "" { |
| log.Print("EQUINIX_DEVICE_OS must be set.") |
| return false |
| } |
| return true |
| } |
| |
| // awaitDeviceState returns nil after device matching the id reaches one of the |
| // provided states. It will return a non-nil value in case of an API error, and |
| // particularly if there exists no device matching id. |
| func awaitDeviceState(ctx context.Context, t *testing.T, cl *client, id string, states ...string) error { |
| if t != nil { |
| t.Helper() |
| } |
| |
| for { |
| d, err := cl.GetDevice(ctx, apipid, id) |
| if err != nil { |
| if errors.Is(err, os.ErrDeadlineExceeded) { |
| continue |
| } |
| return fmt.Errorf("while fetching device info: %w", err) |
| } |
| if d == nil { |
| return fmt.Errorf("expected the test device (ID: %s) to exist.", id) |
| } |
| for _, s := range states { |
| if d.State == s { |
| return nil |
| } |
| } |
| log.Printf("Waiting for device to be provisioned (ID: %s, current state: %q)", id, d.State) |
| time.Sleep(time.Second) |
| } |
| } |
| |
| // cleanup ensures both the test device and the test key are deleted at |
| // Equinix. |
| func cleanup(ctx context.Context, cl *client) { |
| log.Print("Cleaning up.") |
| |
| // Ensure the device matching testDeviceHostname is deleted. |
| ds, err := cl.ListDevices(ctx, apipid) |
| if err != nil { |
| log.Fatalf("while listing devices: %v", err) |
| } |
| var td *packngo.Device |
| for _, d := range ds { |
| if d.Hostname == testDeviceHostname { |
| td = &d |
| break |
| } |
| } |
| if td != nil { |
| log.Printf("Found a test device (ID: %s) that needs to be deleted before progressing further.", td.ID) |
| |
| // Devices currently being provisioned can't be deleted. After it's |
| // provisioned, device's state will match either "active", or "failed". |
| if err := awaitDeviceState(ctx, nil, cl, td.ID, "active", "failed"); err != nil { |
| log.Fatalf("while waiting for device to be provisioned: %v", err) |
| } |
| if err := cl.deleteDevice(ctx, td.ID); err != nil { |
| log.Fatalf("while deleting test device: %v", err) |
| } |
| } |
| |
| // Ensure the key matching sshKeyLabel is deleted. |
| ks, err := cl.ListSSHKeys(ctx) |
| if err != nil { |
| log.Fatalf("while listing SSH keys: %v", err) |
| } |
| for _, k := range ks { |
| if k.Label == sshKeyLabel { |
| log.Printf("Found a SSH test key (ID: %s) - deleting...", k.ID) |
| if err := cl.deleteSSHKey(ctx, k.ID); err != nil { |
| log.Fatalf("while deleting an SSH key: %v", err) |
| } |
| log.Printf("Deleted a SSH test key (ID: %s).", k.ID) |
| } |
| } |
| } |
| |
| func TestMain(m *testing.M) { |
| if !ensureParams() { |
| log.Print("Skipping due to missing parameters.") |
| return |
| } |
| ctx = context.Background() |
| |
| cl := new(&Opts{ |
| User: apiuser, |
| APIKey: apikey, |
| }) |
| defer cl.Close() |
| |
| cleanup(ctx, cl) |
| code := m.Run() |
| cleanup(ctx, cl) |
| os.Exit(code) |
| } |
| |
| // Most test cases depend on the preceding cases having been executed. The |
| // test cases can't be run in parallel. |
| |
| func TestListReservations(t *testing.T) { |
| cl := new(&Opts{ |
| User: apiuser, |
| APIKey: apikey, |
| }) |
| |
| _, err := cl.ListReservations(ctx, apipid) |
| if err != nil { |
| t.Errorf("while listing hardware reservations: %v", err) |
| } |
| } |
| |
| // createSSHAuthKey returns an SSH public key in OpenSSH authorized_keys |
| // format. |
| func createSSHAuthKey(t *testing.T) string { |
| t.Helper() |
| pub, _, err := ed25519.GenerateKey(rand.Reader) |
| if err != nil { |
| t.Errorf("while generating SSH key: %v", err) |
| } |
| |
| sshpub, err := ssh.NewPublicKey(pub) |
| if err != nil { |
| t.Errorf("while generating SSH public key: %v", err) |
| } |
| return string(ssh.MarshalAuthorizedKey(sshpub)) |
| } |
| |
| func TestCreateSSHKey(t *testing.T) { |
| cl := new(&Opts{ |
| User: apiuser, |
| APIKey: apikey, |
| }) |
| |
| nk, err := cl.CreateSSHKey(ctx, &packngo.SSHKeyCreateRequest{ |
| Label: sshKeyLabel, |
| Key: createSSHAuthKey(t), |
| ProjectID: apipid, |
| }) |
| if err != nil { |
| t.Errorf("while creating an SSH key: %v", err) |
| } |
| if nk.Label != sshKeyLabel { |
| t.Errorf("key labels don't match.") |
| } |
| t.Logf("Created an SSH key (ID: %s)", nk.ID) |
| sshKeyID = nk.ID |
| } |
| |
| var ( |
| // dummySSHPK2 is the alternate key used to exercise TestUpdateSSHKey and |
| // TestGetSSHKey. |
| dummySSHPK2 string |
| ) |
| |
| func TestUpdateSSHKey(t *testing.T) { |
| cl := new(&Opts{ |
| User: apiuser, |
| APIKey: apikey, |
| }) |
| |
| if sshKeyID == "" { |
| t.Skip("SSH key couldn't have been created - skipping...") |
| } |
| |
| dummySSHPK2 = createSSHAuthKey(t) |
| k, err := cl.UpdateSSHKey(ctx, sshKeyID, &packngo.SSHKeyUpdateRequest{ |
| Key: &dummySSHPK2, |
| }) |
| if err != nil { |
| t.Errorf("while updating an SSH key: %v", err) |
| } |
| if k.Key != dummySSHPK2 { |
| t.Errorf("updated SSH key doesn't match the original.") |
| } |
| } |
| |
| func TestGetSSHKey(t *testing.T) { |
| cl := new(&Opts{ |
| User: apiuser, |
| APIKey: apikey, |
| }) |
| |
| if sshKeyID == "" { |
| t.Skip("SSH key couldn't have been created - skipping...") |
| } |
| |
| k, err := cl.getSSHKey(ctx, sshKeyID) |
| if err != nil { |
| t.Errorf("while getting an SSH key: %v", err) |
| } |
| if k.Key != dummySSHPK2 { |
| t.Errorf("got key contents that don't match the original.") |
| } |
| } |
| |
| func TestListSSHKeys(t *testing.T) { |
| cl := new(&Opts{ |
| User: apiuser, |
| APIKey: apikey, |
| }) |
| |
| if sshKeyID == "" { |
| t.Skip("SSH key couldn't have been created - skipping...") |
| } |
| |
| ks, err := cl.ListSSHKeys(ctx) |
| if err != nil { |
| t.Errorf("while listing SSH keys: %v", err) |
| } |
| |
| // Check that our key is part of the list. |
| found := false |
| for _, k := range ks { |
| if k.ID == sshKeyID { |
| found = true |
| break |
| } |
| } |
| if !found { |
| t.Errorf("SSH key not listed.") |
| } |
| } |
| |
| func TestCreateDevice(t *testing.T) { |
| cl := new(&Opts{ |
| User: apiuser, |
| APIKey: apikey, |
| }) |
| |
| // Find a provisionable hardware reservation the device will be created with. |
| rvs, err := cl.ListReservations(ctx, apipid) |
| if err != nil { |
| t.Errorf("while listing hardware reservations: %v", err) |
| } |
| var rv *packngo.HardwareReservation |
| for _, r := range rvs { |
| if r.Provisionable { |
| rv = &r |
| break |
| } |
| } |
| if rv == nil { |
| t.Skip("could not find a provisionable hardware reservation - skipping...") |
| } |
| |
| d, err := cl.CreateDevice(ctx, &packngo.DeviceCreateRequest{ |
| Hostname: testDeviceHostname, |
| OS: apios, |
| Plan: rv.Plan.Slug, |
| HardwareReservationID: rv.ID, |
| ProjectID: apipid, |
| }) |
| if err != nil { |
| t.Errorf("while creating a device: %v", err) |
| } |
| t.Logf("Created a new test device (ID: %s)", d.ID) |
| testDevice = d |
| } |
| |
| func TestGetDevice(t *testing.T) { |
| if testDevice == nil { |
| t.Skip("the test device couldn't have been created - skipping...") |
| } |
| |
| cl := new(&Opts{ |
| User: apiuser, |
| APIKey: apikey, |
| }) |
| |
| d, err := cl.GetDevice(ctx, apipid, testDevice.ID) |
| if err != nil { |
| t.Errorf("while fetching device info: %v", err) |
| } |
| if d == nil { |
| t.Errorf("expected the test device (ID: %s) to exist.", testDevice.ID) |
| } |
| if d.ID != testDevice.ID { |
| t.Errorf("got device ID that doesn't match the original.") |
| } |
| } |
| |
| func TestListDevices(t *testing.T) { |
| cl := new(&Opts{ |
| User: apiuser, |
| APIKey: apikey, |
| }) |
| |
| ds, err := cl.ListDevices(ctx, apipid) |
| if err != nil { |
| t.Errorf("while listing devices: %v", err) |
| } |
| if len(ds) == 0 { |
| t.Errorf("expected at least one device.") |
| } |
| } |
| |
| func TestDeleteDevice(t *testing.T) { |
| if testDevice == nil { |
| t.Skip("the test device couldn't have been created - skipping...") |
| } |
| |
| cl := new(&Opts{ |
| User: apiuser, |
| APIKey: apikey, |
| }) |
| |
| // Devices currently being provisioned can't be deleted. After it's |
| // provisioned, device's state will match either "active", or "failed". |
| if err := awaitDeviceState(ctx, t, cl, testDevice.ID, "active", "failed"); err != nil { |
| t.Errorf("while waiting for device to be provisioned: %v", err) |
| } |
| t.Logf("Deleting the test device (ID: %s)", testDevice.ID) |
| if err := cl.deleteDevice(ctx, testDevice.ID); err != nil { |
| t.Errorf("while deleting a device: %v", err) |
| } |
| d, err := cl.GetDevice(ctx, apipid, testDevice.ID) |
| if err != nil && !IsNotFound(err) { |
| t.Errorf("while fetching device info: %v", err) |
| } |
| if d != nil { |
| t.Errorf("device should not exist.") |
| } |
| t.Logf("Deleted the test device (ID: %s)", testDevice.ID) |
| } |
| |
| func TestDeleteSSHKey(t *testing.T) { |
| if sshKeyID == "" { |
| t.Skip("SSH key couldn't have been created - skipping...") |
| } |
| |
| cl := new(&Opts{ |
| User: apiuser, |
| APIKey: apikey, |
| }) |
| |
| t.Logf("Deleting the test SSH key (ID: %s)", sshKeyID) |
| if err := cl.deleteSSHKey(ctx, sshKeyID); err != nil { |
| t.Errorf("couldn't delete an SSH key: %v", err) |
| } |
| _, err := cl.getSSHKey(ctx, sshKeyID) |
| if err == nil { |
| t.Errorf("SSH key should not exist") |
| } |
| t.Logf("Deleted the test SSH key (ID: %s)", sshKeyID) |
| } |