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