blob: 20c2f1613227f9e1282ae976fe96d6401be7610a [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"
17 "source.monogon.dev/cloud/bmaas/bmdb"
18 "source.monogon.dev/cloud/bmaas/bmdb/model"
19 "source.monogon.dev/cloud/lib/component"
20)
21
22// fakeSSHClient is an SSHClient that pretends to start an agent, but in reality
23// just responds with what an agent would respond on every execution attempt.
Lorenz Brun595dfe92023-02-21 19:13:02 +010024type fakeSSHClient struct{}
Serge Bazanskicaa12082023-02-16 14:54:04 +010025
Lorenz Brun595dfe92023-02-21 19:13:02 +010026type fakeSSHConnection struct{}
Serge Bazanskicaa12082023-02-16 14:54:04 +010027
28func (f *fakeSSHClient) Dial(ctx context.Context, address, username string, sshkey ssh.Signer, timeout time.Duration) (SSHConnection, error) {
29 return &fakeSSHConnection{}, nil
30}
31
32func (f *fakeSSHConnection) Execute(ctx context.Context, command string, stdin []byte) (stdout []byte, stderr []byte, err error) {
33 var aim apb.TakeoverInit
34 if err := proto.Unmarshal(stdin, &aim); err != nil {
35 return nil, nil, fmt.Errorf("while unmarshaling TakeoverInit message: %v", err)
36 }
37
38 // Agent should send back apb.TakeoverResponse on its standard output.
39 pub, _, err := ed25519.GenerateKey(rand.Reader)
40 if err != nil {
41 return nil, nil, fmt.Errorf("while generating agent public key: %v", err)
42 }
43 arsp := apb.TakeoverResponse{
Lorenz Brun595dfe92023-02-21 19:13:02 +010044 Result: &apb.TakeoverResponse_Success{Success: &apb.TakeoverSuccess{
45 InitMessage: &aim,
46 Key: pub,
47 }},
Serge Bazanskicaa12082023-02-16 14:54:04 +010048 }
49 arspb, err := proto.Marshal(&arsp)
50 if err != nil {
51 return nil, nil, fmt.Errorf("while marshaling TakeoverResponse message: %v", err)
52 }
53 return arspb, nil, nil
54}
55
56func (f *fakeSSHConnection) Upload(ctx context.Context, targetPath string, data []byte) error {
57 if targetPath != "/fake/path" {
58 return fmt.Errorf("unexpected target path in test")
59 }
60 return nil
61}
62
63func (f *fakeSSHConnection) Close() error {
64 return nil
65}
66
Serge Bazanski86a714d2023-04-17 15:54:21 +020067type initializerDut struct {
68 f *fakequinix
69 i *Initializer
70 bmdb *bmdb.Connection
71 ctx context.Context
72}
73
74func newInitializerDut(t *testing.T) *initializerDut {
75 t.Helper()
76
Serge Bazanskicaa12082023-02-16 14:54:04 +010077 _, key, _ := ed25519.GenerateKey(rand.Reader)
78 sc := SharedConfig{
79 ProjectId: "noproject",
80 KeyLabel: "somekey",
81 Key: key,
82 DevicePrefix: "test-",
83 }
Serge Bazanski86a714d2023-04-17 15:54:21 +020084 ic := InitializerConfig{
85 ControlLoopConfig: ControlLoopConfig{
86 DBQueryLimiter: rate.NewLimiter(rate.Every(time.Second), 10),
87 },
Serge Bazanskicaa12082023-02-16 14:54:04 +010088 Executable: []byte("beep boop i'm a real program"),
89 TargetPath: "/fake/path",
90 Endpoint: "example.com:1234",
91 SSHConnectTimeout: time.Second,
92 SSHExecTimeout: time.Second,
93 }
Serge Bazanski86a714d2023-04-17 15:54:21 +020094
Serge Bazanskicaa12082023-02-16 14:54:04 +010095 f := newFakequinix(sc.ProjectId, 100)
Serge Bazanski86a714d2023-04-17 15:54:21 +020096 i, err := NewInitializer(f, ic, &sc)
Serge Bazanskicaa12082023-02-16 14:54:04 +010097 if err != nil {
98 t.Fatalf("Could not create Initializer: %v", err)
99 }
100
Serge Bazanskicaa12082023-02-16 14:54:04 +0100101 b := bmdb.BMDB{
102 Config: bmdb.Config{
103 Database: component.CockroachConfig{
104 InMemory: true,
105 },
106 ComponentName: "test",
107 RuntimeInfo: "test",
108 },
109 }
110 conn, err := b.Open(true)
111 if err != nil {
112 t.Fatalf("Could not create in-memory BMDB: %v", err)
113 }
114
Serge Bazanski86a714d2023-04-17 15:54:21 +0200115 ctx, ctxC := context.WithCancel(context.Background())
116 t.Cleanup(ctxC)
117
Serge Bazanskicaa12082023-02-16 14:54:04 +0100118 if err := sc.SSHEquinixEnsure(ctx, f); err != nil {
119 t.Fatalf("Failed to ensure SSH key: %v", err)
120 }
121
122 i.sshClient = &fakeSSHClient{}
Serge Bazanski86a714d2023-04-17 15:54:21 +0200123 go RunControlLoop(ctx, conn, i)
124
125 return &initializerDut{
126 f: f,
127 i: i,
128 bmdb: conn,
129 ctx: ctx,
130 }
131}
132
133// TestInitializerSmokes makes sure the Initializer doesn't go up in flames on
134// the happy path.
135func TestInitializerSmokes(t *testing.T) {
136 dut := newInitializerDut(t)
137 f := dut.f
138 ctx := dut.ctx
139 conn := dut.bmdb
140 sc := dut.i.sharedConfig
Serge Bazanskicaa12082023-02-16 14:54:04 +0100141
142 reservations, _ := f.ListReservations(ctx, sc.ProjectId)
143 kid, err := sc.sshEquinixId(ctx, f)
144 if err != nil {
145 t.Fatalf("Failed to retrieve equinix key ID: %v", err)
146 }
147 sess, err := conn.StartSession(ctx)
148 if err != nil {
149 t.Fatalf("Failed to create BMDB session for verifiaction: %v", err)
150 }
151
152 // Create 10 provided machines for testing.
153 for i := 0; i < 10; i++ {
154 res := reservations[i]
155 dev, _ := f.CreateDevice(ctx, &packngo.DeviceCreateRequest{
156 Hostname: fmt.Sprintf("test-%d", i),
157 OS: "fake",
158 ProjectID: sc.ProjectId,
159 HardwareReservationID: res.ID,
160 ProjectSSHKeys: []string{kid},
161 })
162 f.devices[dev.ID].Network = []*packngo.IPAddressAssignment{
163 {
164 IpAddressCommon: packngo.IpAddressCommon{
165 ID: "fake",
166 Address: "1.2.3.4",
167 Management: true,
168 AddressFamily: 4,
169 Public: true,
170 },
171 },
172 }
173 err = sess.Transact(ctx, func(q *model.Queries) error {
174 machine, err := q.NewMachine(ctx)
175 if err != nil {
176 return err
177 }
178 return q.MachineAddProvided(ctx, model.MachineAddProvidedParams{
179 MachineID: machine.MachineID,
180 Provider: model.ProviderEquinix,
181 ProviderID: dev.ID,
182 })
183 })
184 if err != nil {
185 t.Fatalf("Failed to create BMDB machine: %v", err)
186 }
187 }
188
Serge Bazanskicaa12082023-02-16 14:54:04 +0100189 // Expect to find 0 machines needing start.
190 for {
191 time.Sleep(100 * time.Millisecond)
192
193 var machines []model.MachineProvided
194 err = sess.Transact(ctx, func(q *model.Queries) error {
195 var err error
196 machines, err = q.GetMachinesForAgentStart(ctx, 100)
197 return err
198 })
199 if err != nil {
200 t.Fatalf("Failed to run Transaction: %v", err)
201 }
202 if len(machines) == 0 {
203 break
204 }
205 }
206}