blob: 82e1f90b21c3ddf76a0d65cced9d78072ad03228 [file] [log] [blame]
Serge Bazanskicaa12082023-02-16 14:54:04 +01001package manager
2
3import (
4 "context"
5 "crypto/ed25519"
6 "crypto/rand"
7 "fmt"
8 "testing"
9 "time"
10
11 "github.com/packethost/packngo"
12 "golang.org/x/crypto/ssh"
13 "golang.org/x/time/rate"
14 "google.golang.org/protobuf/proto"
15
16 apb "source.monogon.dev/cloud/agent/api"
Tim Windelschmidt0e749612023-08-07 17:42:59 +000017
Serge Bazanskicaa12082023-02-16 14:54:04 +010018 "source.monogon.dev/cloud/bmaas/bmdb"
19 "source.monogon.dev/cloud/bmaas/bmdb/model"
20 "source.monogon.dev/cloud/lib/component"
21)
22
23// fakeSSHClient is an SSHClient that pretends to start an agent, but in reality
24// just responds with what an agent would respond on every execution attempt.
Lorenz Brun595dfe92023-02-21 19:13:02 +010025type fakeSSHClient struct{}
Serge Bazanskicaa12082023-02-16 14:54:04 +010026
Lorenz Brun595dfe92023-02-21 19:13:02 +010027type fakeSSHConnection struct{}
Serge Bazanskicaa12082023-02-16 14:54:04 +010028
29func (f *fakeSSHClient) Dial(ctx context.Context, address, username string, sshkey ssh.Signer, timeout time.Duration) (SSHConnection, error) {
30 return &fakeSSHConnection{}, nil
31}
32
33func (f *fakeSSHConnection) Execute(ctx context.Context, command string, stdin []byte) (stdout []byte, stderr []byte, err error) {
34 var aim apb.TakeoverInit
35 if err := proto.Unmarshal(stdin, &aim); err != nil {
36 return nil, nil, fmt.Errorf("while unmarshaling TakeoverInit message: %v", err)
37 }
38
39 // Agent should send back apb.TakeoverResponse on its standard output.
40 pub, _, err := ed25519.GenerateKey(rand.Reader)
41 if err != nil {
42 return nil, nil, fmt.Errorf("while generating agent public key: %v", err)
43 }
44 arsp := apb.TakeoverResponse{
Lorenz Brun595dfe92023-02-21 19:13:02 +010045 Result: &apb.TakeoverResponse_Success{Success: &apb.TakeoverSuccess{
46 InitMessage: &aim,
47 Key: pub,
48 }},
Serge Bazanskicaa12082023-02-16 14:54:04 +010049 }
50 arspb, err := proto.Marshal(&arsp)
51 if err != nil {
52 return nil, nil, fmt.Errorf("while marshaling TakeoverResponse message: %v", err)
53 }
54 return arspb, nil, nil
55}
56
57func (f *fakeSSHConnection) Upload(ctx context.Context, targetPath string, data []byte) error {
58 if targetPath != "/fake/path" {
59 return fmt.Errorf("unexpected target path in test")
60 }
61 return nil
62}
63
64func (f *fakeSSHConnection) Close() error {
65 return nil
66}
67
Serge Bazanski86a714d2023-04-17 15:54:21 +020068type initializerDut struct {
69 f *fakequinix
70 i *Initializer
71 bmdb *bmdb.Connection
72 ctx context.Context
73}
74
75func newInitializerDut(t *testing.T) *initializerDut {
76 t.Helper()
77
Serge Bazanskicaa12082023-02-16 14:54:04 +010078 _, key, _ := ed25519.GenerateKey(rand.Reader)
79 sc := SharedConfig{
80 ProjectId: "noproject",
81 KeyLabel: "somekey",
82 Key: key,
83 DevicePrefix: "test-",
84 }
Serge Bazanski86a714d2023-04-17 15:54:21 +020085 ic := InitializerConfig{
86 ControlLoopConfig: ControlLoopConfig{
87 DBQueryLimiter: rate.NewLimiter(rate.Every(time.Second), 10),
88 },
Serge Bazanskicaa12082023-02-16 14:54:04 +010089 Executable: []byte("beep boop i'm a real program"),
90 TargetPath: "/fake/path",
91 Endpoint: "example.com:1234",
92 SSHConnectTimeout: time.Second,
93 SSHExecTimeout: time.Second,
94 }
Serge Bazanski86a714d2023-04-17 15:54:21 +020095
Serge Bazanskicaa12082023-02-16 14:54:04 +010096 f := newFakequinix(sc.ProjectId, 100)
Serge Bazanski86a714d2023-04-17 15:54:21 +020097 i, err := NewInitializer(f, ic, &sc)
Serge Bazanskicaa12082023-02-16 14:54:04 +010098 if err != nil {
99 t.Fatalf("Could not create Initializer: %v", err)
100 }
101
Serge Bazanskicaa12082023-02-16 14:54:04 +0100102 b := bmdb.BMDB{
103 Config: bmdb.Config{
104 Database: component.CockroachConfig{
105 InMemory: true,
106 },
107 ComponentName: "test",
108 RuntimeInfo: "test",
109 },
110 }
111 conn, err := b.Open(true)
112 if err != nil {
113 t.Fatalf("Could not create in-memory BMDB: %v", err)
114 }
115
Serge Bazanski86a714d2023-04-17 15:54:21 +0200116 ctx, ctxC := context.WithCancel(context.Background())
117 t.Cleanup(ctxC)
118
Serge Bazanskicaa12082023-02-16 14:54:04 +0100119 if err := sc.SSHEquinixEnsure(ctx, f); err != nil {
120 t.Fatalf("Failed to ensure SSH key: %v", err)
121 }
122
123 i.sshClient = &fakeSSHClient{}
Serge Bazanski86a714d2023-04-17 15:54:21 +0200124 go RunControlLoop(ctx, conn, i)
125
126 return &initializerDut{
127 f: f,
128 i: i,
129 bmdb: conn,
130 ctx: ctx,
131 }
132}
133
134// TestInitializerSmokes makes sure the Initializer doesn't go up in flames on
135// the happy path.
136func TestInitializerSmokes(t *testing.T) {
137 dut := newInitializerDut(t)
138 f := dut.f
139 ctx := dut.ctx
140 conn := dut.bmdb
141 sc := dut.i.sharedConfig
Serge Bazanskicaa12082023-02-16 14:54:04 +0100142
143 reservations, _ := f.ListReservations(ctx, sc.ProjectId)
144 kid, err := sc.sshEquinixId(ctx, f)
145 if err != nil {
146 t.Fatalf("Failed to retrieve equinix key ID: %v", err)
147 }
148 sess, err := conn.StartSession(ctx)
149 if err != nil {
150 t.Fatalf("Failed to create BMDB session for verifiaction: %v", err)
151 }
152
153 // Create 10 provided machines for testing.
154 for i := 0; i < 10; i++ {
155 res := reservations[i]
156 dev, _ := f.CreateDevice(ctx, &packngo.DeviceCreateRequest{
157 Hostname: fmt.Sprintf("test-%d", i),
158 OS: "fake",
159 ProjectID: sc.ProjectId,
160 HardwareReservationID: res.ID,
161 ProjectSSHKeys: []string{kid},
162 })
163 f.devices[dev.ID].Network = []*packngo.IPAddressAssignment{
164 {
165 IpAddressCommon: packngo.IpAddressCommon{
166 ID: "fake",
167 Address: "1.2.3.4",
168 Management: true,
169 AddressFamily: 4,
170 Public: true,
171 },
172 },
173 }
174 err = sess.Transact(ctx, func(q *model.Queries) error {
175 machine, err := q.NewMachine(ctx)
176 if err != nil {
177 return err
178 }
179 return q.MachineAddProvided(ctx, model.MachineAddProvidedParams{
180 MachineID: machine.MachineID,
181 Provider: model.ProviderEquinix,
182 ProviderID: dev.ID,
183 })
184 })
185 if err != nil {
186 t.Fatalf("Failed to create BMDB machine: %v", err)
187 }
188 }
189
Serge Bazanskicaa12082023-02-16 14:54:04 +0100190 // Expect to find 0 machines needing start.
191 for {
192 time.Sleep(100 * time.Millisecond)
193
194 var machines []model.MachineProvided
195 err = sess.Transact(ctx, func(q *model.Queries) error {
196 var err error
Tim Windelschmidt0e749612023-08-07 17:42:59 +0000197 machines, err = q.GetMachinesForAgentStart(ctx, model.GetMachinesForAgentStartParams{
198 Limit: 100,
199 Provider: model.ProviderEquinix,
200 })
Serge Bazanskicaa12082023-02-16 14:54:04 +0100201 return err
202 })
203 if err != nil {
204 t.Fatalf("Failed to run Transaction: %v", err)
205 }
206 if len(machines) == 0 {
207 break
208 }
209 }
210}