blob: d69ff4e2177fc497ac95f58b47b7c0395220d746 [file] [log] [blame]
Mateusz Zalega6a058e72022-11-30 18:03:07 +01001package wrapngo
2
3import (
4 "context"
5 "crypto/ed25519"
6 "crypto/rand"
7 "errors"
8 "fmt"
9 "log"
10 "os"
11 "testing"
12 "time"
13
14 "github.com/packethost/packngo"
15 "golang.org/x/crypto/ssh"
16)
17
18var (
19 ctx context.Context
20
21 // apiuser and apikey are the Equinix credentials necessary to test the
22 // client package.
23
24 apiuser string = os.Getenv("EQUINIX_USER")
25 apikey string = os.Getenv("EQUINIX_APIKEY")
26
27 // apipid references the Equinix Metal project used. It's recommended to use
28 // non-production projects in the context of testing.
29 apipid string = os.Getenv("EQUINIX_PROJECT_ID")
30 // apios specifies the operating system installed on newly provisioned
31 // devices. See Equinix Metal API documentation for details.
32 apios string = os.Getenv("EQUINIX_DEVICE_OS")
33
34 // sshKeyID identifies the SSH public key registered with Equinix.
35 sshKeyID string
36 // sshKeyLabel is the label used to register the SSH key with Equinix.
37 sshKeyLabel string = "shepherd-client-testkey"
38
39 // testDevice is the device created in TestCreateDevice, and later
40 // referenced to exercise implementation operating on Equinix Metal device
41 // objects.
42 testDevice *packngo.Device
43 // testDeviceHostname is the hostname used to register and reference the
44 // test device.
45 testDeviceHostname string = "shepherd-client-testdev"
46)
47
48// ensureParams returns false if any of the required environment variable
49// parameters are missing.
50func ensureParams() bool {
51 if apiuser == "" {
52 log.Print("EQUINIX_USER must be set.")
53 return false
54 }
55 if apikey == "" {
56 log.Print("EQUINIX_APIKEY must be set.")
57 return false
58 }
59 if apipid == "" {
60 log.Print("EQUINIX_PROJECT_ID must be set.")
61 return false
62 }
63 if apios == "" {
64 log.Print("EQUINIX_DEVICE_OS must be set.")
65 return false
66 }
67 return true
68}
69
70// awaitDeviceState returns nil after device matching the id reaches one of the
71// provided states. It will return a non-nil value in case of an API error, and
72// particularly if there exists no device matching id.
73func awaitDeviceState(ctx context.Context, t *testing.T, cl *client, id string, states ...string) error {
74 if t != nil {
75 t.Helper()
76 }
77
78 for {
79 d, err := cl.GetDevice(ctx, apipid, id)
80 if err != nil {
81 if errors.Is(err, os.ErrDeadlineExceeded) {
82 continue
83 }
84 return fmt.Errorf("while fetching device info: %w", err)
85 }
86 if d == nil {
87 return fmt.Errorf("expected the test device (ID: %s) to exist.", id)
88 }
89 for _, s := range states {
90 if d.State == s {
91 return nil
92 }
93 }
94 log.Printf("Waiting for device to be provisioned (ID: %s, current state: %q)", id, d.State)
95 time.Sleep(time.Second)
96 }
97}
98
99// cleanup ensures both the test device and the test key are deleted at
100// Equinix.
101func cleanup(ctx context.Context, cl *client) {
102 log.Print("Cleaning up.")
103
104 // Ensure the device matching testDeviceHostname is deleted.
105 ds, err := cl.ListDevices(ctx, apipid)
106 if err != nil {
107 log.Fatalf("while listing devices: %v", err)
108 }
109 var td *packngo.Device
110 for _, d := range ds {
111 if d.Hostname == testDeviceHostname {
112 td = &d
113 break
114 }
115 }
116 if td != nil {
117 log.Printf("Found a test device (ID: %s) that needs to be deleted before progressing further.", td.ID)
118
119 // Devices currently being provisioned can't be deleted. After it's
120 // provisioned, device's state will match either "active", or "failed".
121 if err := awaitDeviceState(ctx, nil, cl, td.ID, "active", "failed"); err != nil {
122 log.Fatalf("while waiting for device to be provisioned: %v", err)
123 }
124 if err := cl.deleteDevice(ctx, td.ID); err != nil {
125 log.Fatalf("while deleting test device: %v", err)
126 }
127 }
128
129 // Ensure the key matching sshKeyLabel is deleted.
130 ks, err := cl.ListSSHKeys(ctx)
131 if err != nil {
132 log.Fatalf("while listing SSH keys: %v", err)
133 }
134 for _, k := range ks {
135 if k.Label == sshKeyLabel {
136 log.Printf("Found a SSH test key (ID: %s) - deleting...", k.ID)
137 if err := cl.deleteSSHKey(ctx, k.ID); err != nil {
138 log.Fatalf("while deleting an SSH key: %v", err)
139 }
140 log.Printf("Deleted a SSH test key (ID: %s).", k.ID)
141 }
142 }
143}
144
145func TestMain(m *testing.M) {
146 if !ensureParams() {
147 log.Print("Skipping due to missing parameters.")
148 return
149 }
150 ctx = context.Background()
151
152 cl := new(&Opts{
153 User: apiuser,
154 APIKey: apikey,
155 })
156 defer cl.Close()
157
158 cleanup(ctx, cl)
159 code := m.Run()
160 cleanup(ctx, cl)
161 os.Exit(code)
162}
163
164// Most test cases depend on the preceding cases having been executed. The
165// test cases can't be run in parallel.
166
167func TestListReservations(t *testing.T) {
168 cl := new(&Opts{
169 User: apiuser,
170 APIKey: apikey,
171 })
172
173 _, err := cl.ListReservations(ctx, apipid)
174 if err != nil {
175 t.Errorf("while listing hardware reservations: %v", err)
176 }
177}
178
179// createSSHAuthKey returns an SSH public key in OpenSSH authorized_keys
180// format.
181func createSSHAuthKey(t *testing.T) string {
182 t.Helper()
183 pub, _, err := ed25519.GenerateKey(rand.Reader)
184 if err != nil {
185 t.Errorf("while generating SSH key: %v", err)
186 }
187
188 sshpub, err := ssh.NewPublicKey(pub)
189 if err != nil {
190 t.Errorf("while generating SSH public key: %v", err)
191 }
192 return string(ssh.MarshalAuthorizedKey(sshpub))
193}
194
195func TestCreateSSHKey(t *testing.T) {
196 cl := new(&Opts{
197 User: apiuser,
198 APIKey: apikey,
199 })
200
201 nk, err := cl.CreateSSHKey(ctx, &packngo.SSHKeyCreateRequest{
202 Label: sshKeyLabel,
203 Key: createSSHAuthKey(t),
204 ProjectID: apipid,
205 })
206 if err != nil {
207 t.Errorf("while creating an SSH key: %v", err)
208 }
209 if nk.Label != sshKeyLabel {
210 t.Errorf("key labels don't match.")
211 }
212 t.Logf("Created an SSH key (ID: %s)", nk.ID)
213 sshKeyID = nk.ID
214}
215
216var (
217 // dummySSHPK2 is the alternate key used to exercise TestUpdateSSHKey and
218 // TestGetSSHKey.
219 dummySSHPK2 string
220)
221
222func TestUpdateSSHKey(t *testing.T) {
223 cl := new(&Opts{
224 User: apiuser,
225 APIKey: apikey,
226 })
227
228 if sshKeyID == "" {
229 t.Skip("SSH key couldn't have been created - skipping...")
230 }
231
232 dummySSHPK2 = createSSHAuthKey(t)
233 k, err := cl.UpdateSSHKey(ctx, sshKeyID, &packngo.SSHKeyUpdateRequest{
234 Key: &dummySSHPK2,
235 })
236 if err != nil {
237 t.Errorf("while updating an SSH key: %v", err)
238 }
239 if k.Key != dummySSHPK2 {
240 t.Errorf("updated SSH key doesn't match the original.")
241 }
242}
243
244func TestGetSSHKey(t *testing.T) {
245 cl := new(&Opts{
246 User: apiuser,
247 APIKey: apikey,
248 })
249
250 if sshKeyID == "" {
251 t.Skip("SSH key couldn't have been created - skipping...")
252 }
253
254 k, err := cl.getSSHKey(ctx, sshKeyID)
255 if err != nil {
256 t.Errorf("while getting an SSH key: %v", err)
257 }
258 if k.Key != dummySSHPK2 {
259 t.Errorf("got key contents that don't match the original.")
260 }
261}
262
263func TestListSSHKeys(t *testing.T) {
264 cl := new(&Opts{
265 User: apiuser,
266 APIKey: apikey,
267 })
268
269 if sshKeyID == "" {
270 t.Skip("SSH key couldn't have been created - skipping...")
271 }
272
273 ks, err := cl.ListSSHKeys(ctx)
274 if err != nil {
275 t.Errorf("while listing SSH keys: %v", err)
276 }
277
278 // Check that our key is part of the list.
279 found := false
280 for _, k := range ks {
281 if k.ID == sshKeyID {
282 found = true
283 break
284 }
285 }
286 if !found {
287 t.Errorf("SSH key not listed.")
288 }
289}
290
291func TestCreateDevice(t *testing.T) {
292 cl := new(&Opts{
293 User: apiuser,
294 APIKey: apikey,
295 })
296
297 // Find a provisionable hardware reservation the device will be created with.
298 rvs, err := cl.ListReservations(ctx, apipid)
299 if err != nil {
300 t.Errorf("while listing hardware reservations: %v", err)
301 }
302 var rv *packngo.HardwareReservation
303 for _, r := range rvs {
304 if r.Provisionable {
305 rv = &r
306 break
307 }
308 }
309 if rv == nil {
310 t.Skip("could not find a provisionable hardware reservation - skipping...")
311 }
312
313 d, err := cl.CreateDevice(ctx, &packngo.DeviceCreateRequest{
314 Hostname: testDeviceHostname,
315 OS: apios,
316 Plan: rv.Plan.Slug,
317 HardwareReservationID: rv.ID,
318 ProjectID: apipid,
319 })
320 if err != nil {
321 t.Errorf("while creating a device: %v", err)
322 }
323 t.Logf("Created a new test device (ID: %s)", d.ID)
324 testDevice = d
325}
326
327func TestGetDevice(t *testing.T) {
328 if testDevice == nil {
329 t.Skip("the test device couldn't have been created - skipping...")
330 }
331
332 cl := new(&Opts{
333 User: apiuser,
334 APIKey: apikey,
335 })
336
337 d, err := cl.GetDevice(ctx, apipid, testDevice.ID)
338 if err != nil {
339 t.Errorf("while fetching device info: %v", err)
340 }
341 if d == nil {
342 t.Errorf("expected the test device (ID: %s) to exist.", testDevice.ID)
343 }
344 if d.ID != testDevice.ID {
345 t.Errorf("got device ID that doesn't match the original.")
346 }
347}
348
349func TestListDevices(t *testing.T) {
350 cl := new(&Opts{
351 User: apiuser,
352 APIKey: apikey,
353 })
354
355 ds, err := cl.ListDevices(ctx, apipid)
356 if err != nil {
357 t.Errorf("while listing devices: %v", err)
358 }
359 if len(ds) == 0 {
360 t.Errorf("expected at least one device.")
361 }
362}
363
364func TestDeleteDevice(t *testing.T) {
365 if testDevice == nil {
366 t.Skip("the test device couldn't have been created - skipping...")
367 }
368
369 cl := new(&Opts{
370 User: apiuser,
371 APIKey: apikey,
372 })
373
374 // Devices currently being provisioned can't be deleted. After it's
375 // provisioned, device's state will match either "active", or "failed".
376 if err := awaitDeviceState(ctx, t, cl, testDevice.ID, "active", "failed"); err != nil {
377 t.Errorf("while waiting for device to be provisioned: %v", err)
378 }
379 t.Logf("Deleting the test device (ID: %s)", testDevice.ID)
380 if err := cl.deleteDevice(ctx, testDevice.ID); err != nil {
381 t.Errorf("while deleting a device: %v", err)
382 }
383 d, err := cl.GetDevice(ctx, apipid, testDevice.ID)
384 if err != nil && !IsNotFound(err) {
385 t.Errorf("while fetching device info: %v", err)
386 }
387 if d != nil {
388 t.Errorf("device should not exist.")
389 }
390 t.Logf("Deleted the test device (ID: %s)", testDevice.ID)
391}
392
393func TestDeleteSSHKey(t *testing.T) {
394 if sshKeyID == "" {
395 t.Skip("SSH key couldn't have been created - skipping...")
396 }
397
398 cl := new(&Opts{
399 User: apiuser,
400 APIKey: apikey,
401 })
402
403 t.Logf("Deleting the test SSH key (ID: %s)", sshKeyID)
404 if err := cl.deleteSSHKey(ctx, sshKeyID); err != nil {
405 t.Errorf("couldn't delete an SSH key: %v", err)
406 }
407 _, err := cl.getSSHKey(ctx, sshKeyID)
408 if err == nil {
409 t.Errorf("SSH key should not exist")
410 }
411 t.Logf("Deleted the test SSH key (ID: %s)", sshKeyID)
412}