| package wrapngo |
| |
| import ( |
| "context" |
| "crypto/ed25519" |
| "crypto/rand" |
| "errors" |
| "fmt" |
| "log" |
| "os" |
| "testing" |
| "time" |
| |
| "github.com/packethost/packngo" |
| "golang.org/x/crypto/ssh" |
| ) |
| |
| type liveTestClient struct { |
| cl *client |
| ctx context.Context |
| |
| apipid string |
| apios string |
| |
| sshKeyLabel string |
| testDeviceHostname string |
| } |
| |
| func newLiveTestClient(t *testing.T) *liveTestClient { |
| t.Helper() |
| |
| apiuser := os.Getenv("EQUINIX_USER") |
| apikey := os.Getenv("EQUINIX_APIKEY") |
| apipid := os.Getenv("EQUINIX_PROJECT_ID") |
| apios := os.Getenv("EQUINIX_DEVICE_OS") |
| |
| if apiuser == "" { |
| t.Skip("EQUINIX_USER must be set.") |
| } |
| if apikey == "" { |
| t.Skip("EQUINIX_APIKEY must be set.") |
| } |
| if apipid == "" { |
| t.Skip("EQUINIX_PROJECT_ID must be set.") |
| } |
| if apios == "" { |
| t.Skip("EQUINIX_DEVICE_OS must be set.") |
| } |
| ctx, ctxC := context.WithCancel(context.Background()) |
| t.Cleanup(ctxC) |
| return &liveTestClient{ |
| cl: new(&Opts{ |
| User: apiuser, |
| APIKey: apikey, |
| }), |
| ctx: ctx, |
| |
| apipid: apipid, |
| apios: apios, |
| |
| sshKeyLabel: "shepherd-livetest-client", |
| testDeviceHostname: "shepherd-livetest-device", |
| } |
| } |
| |
| // 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 (l *liveTestClient) awaitDeviceState(t *testing.T, id string, states ...string) error { |
| t.Helper() |
| |
| for { |
| d, err := l.cl.GetDevice(l.ctx, l.apipid, id, nil) |
| 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 |
| } |
| } |
| t.Logf("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 (l *liveTestClient) cleanup(t *testing.T) { |
| t.Helper() |
| |
| t.Logf("Cleaning up.") |
| |
| // Ensure the device matching testDeviceHostname is deleted. |
| ds, err := l.cl.ListDevices(l.ctx, l.apipid) |
| if err != nil { |
| log.Fatalf("while listing devices: %v", err) |
| } |
| var td *packngo.Device |
| for _, d := range ds { |
| if d.Hostname == l.testDeviceHostname { |
| td = &d |
| break |
| } |
| } |
| if td != nil { |
| t.Logf("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 := l.awaitDeviceState(t, "active", "failed"); err != nil { |
| t.Fatalf("while waiting for device to be provisioned: %v", err) |
| } |
| if err := l.cl.deleteDevice(l.ctx, td.ID); err != nil { |
| t.Fatalf("while deleting test device: %v", err) |
| } |
| } |
| |
| // Ensure the key matching sshKeyLabel is deleted. |
| ks, err := l.cl.ListSSHKeys(l.ctx) |
| if err != nil { |
| t.Fatalf("while listing SSH keys: %v", err) |
| } |
| for _, k := range ks { |
| if k.Label == l.sshKeyLabel { |
| t.Logf("Found a SSH test key (ID: %s) - deleting...", k.ID) |
| if err := l.cl.deleteSSHKey(l.ctx, k.ID); err != nil { |
| t.Fatalf("while deleting an SSH key: %v", err) |
| } |
| t.Logf("Deleted a SSH test key (ID: %s).", k.ID) |
| } |
| } |
| } |
| |
| // 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)) |
| } |
| |
| // TestLiveAPI performs smoke tests of wrapngo against the real Equinix API. See |
| // newLiveTestClient to see which environment variables need to be provided in |
| // order for this test to run. |
| func TestLiveAPI(t *testing.T) { |
| ltc := newLiveTestClient(t) |
| ltc.cleanup(t) |
| |
| cl := ltc.cl |
| ctx := ltc.ctx |
| |
| t.Run("ListReservations", func(t *testing.T) { |
| _, err := cl.ListReservations(ctx, ltc.apipid) |
| if err != nil { |
| t.Errorf("while listing hardware reservations: %v", err) |
| } |
| }) |
| |
| var sshKeyID string |
| t.Run("CreateSSHKey", func(t *testing.T) { |
| nk, err := cl.CreateSSHKey(ctx, &packngo.SSHKeyCreateRequest{ |
| Label: ltc.sshKeyLabel, |
| Key: createSSHAuthKey(t), |
| ProjectID: ltc.apipid, |
| }) |
| if err != nil { |
| t.Fatalf("while creating an SSH key: %v", err) |
| } |
| if nk.Label != ltc.sshKeyLabel { |
| t.Errorf("key labels don't match.") |
| } |
| t.Logf("Created an SSH key (ID: %s)", nk.ID) |
| sshKeyID = nk.ID |
| }) |
| |
| var dummySSHPK2 string |
| t.Run("UpdateSSHKey", func(t *testing.T) { |
| 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.Fatalf("while updating an SSH key: %v", err) |
| } |
| if k.Key != dummySSHPK2 { |
| t.Errorf("updated SSH key doesn't match the original.") |
| } |
| }) |
| t.Run("GetSSHKey", func(t *testing.T) { |
| if sshKeyID == "" { |
| t.Skip("SSH key couldn't have been created - skipping...") |
| } |
| |
| k, err := cl.getSSHKey(ctx, sshKeyID) |
| if err != nil { |
| t.Fatalf("while getting an SSH key: %v", err) |
| } |
| if k.Key != dummySSHPK2 { |
| t.Errorf("got key contents that don't match the original.") |
| } |
| }) |
| t.Run("ListSSHKeys", func(t *testing.T) { |
| if sshKeyID == "" { |
| t.Skip("SSH key couldn't have been created - skipping...") |
| } |
| |
| ks, err := cl.ListSSHKeys(ctx) |
| if err != nil { |
| t.Fatalf("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.") |
| } |
| }) |
| |
| var testDevice *packngo.Device |
| t.Run("CreateDevice", func(t *testing.T) { |
| // Find a provisionable hardware reservation the device will be created with. |
| rvs, err := cl.ListReservations(ctx, ltc.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: ltc.testDeviceHostname, |
| OS: ltc.apios, |
| Plan: rv.Plan.Slug, |
| HardwareReservationID: rv.ID, |
| ProjectID: ltc.apipid, |
| }) |
| if err != nil { |
| t.Fatalf("while creating a device: %v", err) |
| } |
| t.Logf("Created a new test device (ID: %s)", d.ID) |
| testDevice = d |
| }) |
| t.Run("GetDevice", func(t *testing.T) { |
| if testDevice == nil { |
| t.Skip("the test device couldn't have been created - skipping...") |
| } |
| |
| d, err := cl.GetDevice(ctx, ltc.apipid, testDevice.ID, nil) |
| if err != nil { |
| t.Fatalf("while fetching device info: %v", err) |
| } |
| if d == nil { |
| t.Fatalf("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.") |
| } |
| }) |
| t.Run("ListDevices", func(t *testing.T) { |
| if testDevice == nil { |
| t.Skip("the test device couldn't have been created - skipping...") |
| } |
| |
| ds, err := cl.ListDevices(ctx, ltc.apipid) |
| if err != nil { |
| t.Errorf("while listing devices: %v", err) |
| } |
| if len(ds) == 0 { |
| t.Errorf("expected at least one device.") |
| } |
| }) |
| t.Run("DeleteDevice", func(t *testing.T) { |
| if testDevice == nil { |
| t.Skip("the test device couldn't have been created - skipping...") |
| } |
| |
| // Devices currently being provisioned can't be deleted. After it's |
| // provisioned, device's state will match either "active", or "failed". |
| if err := ltc.awaitDeviceState(t, testDevice.ID, "active", "failed"); err != nil { |
| t.Fatalf("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.Fatalf("while deleting a device: %v", err) |
| } |
| d, err := cl.GetDevice(ctx, ltc.apipid, testDevice.ID, nil) |
| if err != nil && !IsNotFound(err) { |
| t.Fatalf("while fetching device info: %v", err) |
| } |
| if d != nil { |
| t.Fatalf("device should not exist.") |
| } |
| t.Logf("Deleted the test device (ID: %s)", testDevice.ID) |
| }) |
| t.Run("DeleteSSHKey", func(t *testing.T) { |
| if sshKeyID == "" { |
| t.Skip("SSH key couldn't have been created - skipping...") |
| } |
| |
| t.Logf("Deleting the test SSH key (ID: %s)", sshKeyID) |
| if err := cl.deleteSSHKey(ctx, sshKeyID); err != nil { |
| t.Fatalf("couldn't delete an SSH key: %v", err) |
| } |
| _, err := cl.getSSHKey(ctx, sshKeyID) |
| if err == nil { |
| t.Fatalf("SSH key should not exist") |
| } |
| t.Logf("Deleted the test SSH key (ID: %s)", sshKeyID) |
| }) |
| |
| ltc.cleanup(t) |
| } |