blob: 5a8d194979fe8e164697e914ab8febc1ff01c1c2 [file] [log] [blame]
Serge Bazanski0d58cb92023-04-17 18:38:56 +02001package 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
18type liveTestClient struct {
19 cl *client
20 ctx context.Context
21
22 apipid string
23 apios string
24
25 sshKeyLabel string
26 testDeviceHostname string
27}
28
29func newLiveTestClient(t *testing.T) *liveTestClient {
30 t.Helper()
31
32 apiuser := os.Getenv("EQUINIX_USER")
33 apikey := os.Getenv("EQUINIX_APIKEY")
34 apipid := os.Getenv("EQUINIX_PROJECT_ID")
35 apios := os.Getenv("EQUINIX_DEVICE_OS")
36
37 if apiuser == "" {
38 t.Skip("EQUINIX_USER must be set.")
39 }
40 if apikey == "" {
41 t.Skip("EQUINIX_APIKEY must be set.")
42 }
43 if apipid == "" {
44 t.Skip("EQUINIX_PROJECT_ID must be set.")
45 }
46 if apios == "" {
47 t.Skip("EQUINIX_DEVICE_OS must be set.")
48 }
49 ctx, ctxC := context.WithCancel(context.Background())
50 t.Cleanup(ctxC)
51 return &liveTestClient{
52 cl: new(&Opts{
53 User: apiuser,
54 APIKey: apikey,
55 }),
56 ctx: ctx,
57
58 apipid: apipid,
59 apios: apios,
60
61 sshKeyLabel: "shepherd-livetest-client",
62 testDeviceHostname: "shepherd-livetest-device",
63 }
64}
65
66// awaitDeviceState returns nil after device matching the id reaches one of the
67// provided states. It will return a non-nil value in case of an API error, and
68// particularly if there exists no device matching id.
69func (l *liveTestClient) awaitDeviceState(t *testing.T, id string, states ...string) error {
70 t.Helper()
71
72 for {
73 d, err := l.cl.GetDevice(l.ctx, l.apipid, id)
74 if err != nil {
75 if errors.Is(err, os.ErrDeadlineExceeded) {
76 continue
77 }
78 return fmt.Errorf("while fetching device info: %w", err)
79 }
80 if d == nil {
81 return fmt.Errorf("expected the test device (ID: %s) to exist.", id)
82 }
83 for _, s := range states {
84 if d.State == s {
85 return nil
86 }
87 }
88 t.Logf("Waiting for device to be provisioned (ID: %s, current state: %q)", id, d.State)
89 time.Sleep(time.Second)
90 }
91}
92
93// cleanup ensures both the test device and the test key are deleted at
94// Equinix.
95func (l *liveTestClient) cleanup(t *testing.T) {
96 t.Helper()
97
98 t.Logf("Cleaning up.")
99
100 // Ensure the device matching testDeviceHostname is deleted.
101 ds, err := l.cl.ListDevices(l.ctx, l.apipid)
102 if err != nil {
103 log.Fatalf("while listing devices: %v", err)
104 }
105 var td *packngo.Device
106 for _, d := range ds {
107 if d.Hostname == l.testDeviceHostname {
108 td = &d
109 break
110 }
111 }
112 if td != nil {
113 t.Logf("Found a test device (ID: %s) that needs to be deleted before progressing further.", td.ID)
114
115 // Devices currently being provisioned can't be deleted. After it's
116 // provisioned, device's state will match either "active", or "failed".
117 if err := l.awaitDeviceState(t, "active", "failed"); err != nil {
118 t.Fatalf("while waiting for device to be provisioned: %v", err)
119 }
120 if err := l.cl.deleteDevice(l.ctx, td.ID); err != nil {
121 t.Fatalf("while deleting test device: %v", err)
122 }
123 }
124
125 // Ensure the key matching sshKeyLabel is deleted.
126 ks, err := l.cl.ListSSHKeys(l.ctx)
127 if err != nil {
128 t.Fatalf("while listing SSH keys: %v", err)
129 }
130 for _, k := range ks {
131 if k.Label == l.sshKeyLabel {
132 t.Logf("Found a SSH test key (ID: %s) - deleting...", k.ID)
133 if err := l.cl.deleteSSHKey(l.ctx, k.ID); err != nil {
134 t.Fatalf("while deleting an SSH key: %v", err)
135 }
136 t.Logf("Deleted a SSH test key (ID: %s).", k.ID)
137 }
138 }
139}
140
141// createSSHAuthKey returns an SSH public key in OpenSSH authorized_keys
142// format.
143func createSSHAuthKey(t *testing.T) string {
144 t.Helper()
145 pub, _, err := ed25519.GenerateKey(rand.Reader)
146 if err != nil {
147 t.Errorf("while generating SSH key: %v", err)
148 }
149
150 sshpub, err := ssh.NewPublicKey(pub)
151 if err != nil {
152 t.Errorf("while generating SSH public key: %v", err)
153 }
154 return string(ssh.MarshalAuthorizedKey(sshpub))
155}
156
157// TestLiveAPI performs smoke tests of wrapngo against the real Equinix API. See
158// newLiveTestClient to see which environment variables need to be provided in
159// order for this test to run.
160func TestLiveAPI(t *testing.T) {
161 ltc := newLiveTestClient(t)
162 ltc.cleanup(t)
163
164 cl := ltc.cl
165 ctx := ltc.ctx
166
167 t.Run("ListReservations", func(t *testing.T) {
168 _, err := cl.ListReservations(ctx, ltc.apipid)
169 if err != nil {
170 t.Errorf("while listing hardware reservations: %v", err)
171 }
172 })
173
174 var sshKeyID string
175 t.Run("CreateSSHKey", func(t *testing.T) {
176 nk, err := cl.CreateSSHKey(ctx, &packngo.SSHKeyCreateRequest{
177 Label: ltc.sshKeyLabel,
178 Key: createSSHAuthKey(t),
179 ProjectID: ltc.apipid,
180 })
181 if err != nil {
182 t.Fatalf("while creating an SSH key: %v", err)
183 }
184 if nk.Label != ltc.sshKeyLabel {
185 t.Errorf("key labels don't match.")
186 }
187 t.Logf("Created an SSH key (ID: %s)", nk.ID)
188 sshKeyID = nk.ID
189 })
190
191 var dummySSHPK2 string
192 t.Run("UpdateSSHKey", func(t *testing.T) {
193 if sshKeyID == "" {
194 t.Skip("SSH key couldn't have been created - skipping...")
195 }
196
197 dummySSHPK2 = createSSHAuthKey(t)
198 k, err := cl.UpdateSSHKey(ctx, sshKeyID, &packngo.SSHKeyUpdateRequest{
199 Key: &dummySSHPK2,
200 })
201 if err != nil {
202 t.Fatalf("while updating an SSH key: %v", err)
203 }
204 if k.Key != dummySSHPK2 {
205 t.Errorf("updated SSH key doesn't match the original.")
206 }
207 })
208 t.Run("GetSSHKey", func(t *testing.T) {
209 if sshKeyID == "" {
210 t.Skip("SSH key couldn't have been created - skipping...")
211 }
212
213 k, err := cl.getSSHKey(ctx, sshKeyID)
214 if err != nil {
215 t.Fatalf("while getting an SSH key: %v", err)
216 }
217 if k.Key != dummySSHPK2 {
218 t.Errorf("got key contents that don't match the original.")
219 }
220 })
221 t.Run("ListSSHKeys", func(t *testing.T) {
222 if sshKeyID == "" {
223 t.Skip("SSH key couldn't have been created - skipping...")
224 }
225
226 ks, err := cl.ListSSHKeys(ctx)
227 if err != nil {
228 t.Fatalf("while listing SSH keys: %v", err)
229 }
230
231 // Check that our key is part of the list.
232 found := false
233 for _, k := range ks {
234 if k.ID == sshKeyID {
235 found = true
236 break
237 }
238 }
239 if !found {
240 t.Errorf("SSH key not listed.")
241 }
242 })
243
244 var testDevice *packngo.Device
245 t.Run("CreateDevice", func(t *testing.T) {
246 // Find a provisionable hardware reservation the device will be created with.
247 rvs, err := cl.ListReservations(ctx, ltc.apipid)
248 if err != nil {
249 t.Errorf("while listing hardware reservations: %v", err)
250 }
251 var rv *packngo.HardwareReservation
252 for _, r := range rvs {
253 if r.Provisionable {
254 rv = &r
255 break
256 }
257 }
258 if rv == nil {
259 t.Skip("could not find a provisionable hardware reservation - skipping...")
260 }
261
262 d, err := cl.CreateDevice(ctx, &packngo.DeviceCreateRequest{
263 Hostname: ltc.testDeviceHostname,
264 OS: ltc.apios,
265 Plan: rv.Plan.Slug,
266 HardwareReservationID: rv.ID,
267 ProjectID: ltc.apipid,
268 })
269 if err != nil {
270 t.Fatalf("while creating a device: %v", err)
271 }
272 t.Logf("Created a new test device (ID: %s)", d.ID)
273 testDevice = d
274 })
275 t.Run("GetDevice", func(t *testing.T) {
276 if testDevice == nil {
277 t.Skip("the test device couldn't have been created - skipping...")
278 }
279
280 d, err := cl.GetDevice(ctx, ltc.apipid, testDevice.ID)
281 if err != nil {
282 t.Fatalf("while fetching device info: %v", err)
283 }
284 if d == nil {
285 t.Fatalf("expected the test device (ID: %s) to exist.", testDevice.ID)
286 }
287 if d.ID != testDevice.ID {
288 t.Errorf("got device ID that doesn't match the original.")
289 }
290 })
291 t.Run("ListDevices", func(t *testing.T) {
292 if testDevice == nil {
293 t.Skip("the test device couldn't have been created - skipping...")
294 }
295
296 ds, err := cl.ListDevices(ctx, ltc.apipid)
297 if err != nil {
298 t.Errorf("while listing devices: %v", err)
299 }
300 if len(ds) == 0 {
301 t.Errorf("expected at least one device.")
302 }
303 })
304 t.Run("DeleteDevice", func(t *testing.T) {
305 if testDevice == nil {
306 t.Skip("the test device couldn't have been created - skipping...")
307 }
308
309 // Devices currently being provisioned can't be deleted. After it's
310 // provisioned, device's state will match either "active", or "failed".
311 if err := ltc.awaitDeviceState(t, testDevice.ID, "active", "failed"); err != nil {
312 t.Fatalf("while waiting for device to be provisioned: %v", err)
313 }
314 t.Logf("Deleting the test device (ID: %s)", testDevice.ID)
315 if err := cl.deleteDevice(ctx, testDevice.ID); err != nil {
316 t.Fatalf("while deleting a device: %v", err)
317 }
318 d, err := cl.GetDevice(ctx, ltc.apipid, testDevice.ID)
319 if err != nil && !IsNotFound(err) {
320 t.Fatalf("while fetching device info: %v", err)
321 }
322 if d != nil {
323 t.Fatalf("device should not exist.")
324 }
325 t.Logf("Deleted the test device (ID: %s)", testDevice.ID)
326 })
327 t.Run("DeleteSSHKey", func(t *testing.T) {
328 if sshKeyID == "" {
329 t.Skip("SSH key couldn't have been created - skipping...")
330 }
331
332 t.Logf("Deleting the test SSH key (ID: %s)", sshKeyID)
333 if err := cl.deleteSSHKey(ctx, sshKeyID); err != nil {
334 t.Fatalf("couldn't delete an SSH key: %v", err)
335 }
336 _, err := cl.getSSHKey(ctx, sshKeyID)
337 if err == nil {
338 t.Fatalf("SSH key should not exist")
339 }
340 t.Logf("Deleted the test SSH key (ID: %s)", sshKeyID)
341 })
342
343 ltc.cleanup(t)
344}