blob: 6e82b983cfa97d92c6596dbe7fe39d06b971aa30 [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
67// TestInitializerSmokes makes sure the Initializer doesn't go up in flames on
68// the happy path.
69func TestInitializerSmokes(t *testing.T) {
70 ic := InitializerConfig{
71 DBQueryLimiter: rate.NewLimiter(rate.Every(time.Second), 10),
72 }
73 _, key, _ := ed25519.GenerateKey(rand.Reader)
74 sc := SharedConfig{
75 ProjectId: "noproject",
76 KeyLabel: "somekey",
77 Key: key,
78 DevicePrefix: "test-",
79 }
80 ac := AgentConfig{
81 Executable: []byte("beep boop i'm a real program"),
82 TargetPath: "/fake/path",
83 Endpoint: "example.com:1234",
84 SSHConnectTimeout: time.Second,
85 SSHExecTimeout: time.Second,
86 }
87 f := newFakequinix(sc.ProjectId, 100)
88 i, err := ic.New(f, &sc, &ac)
89 if err != nil {
90 t.Fatalf("Could not create Initializer: %v", err)
91 }
92
93 ctx, ctxC := context.WithCancel(context.Background())
94 defer ctxC()
95
96 b := bmdb.BMDB{
97 Config: bmdb.Config{
98 Database: component.CockroachConfig{
99 InMemory: true,
100 },
101 ComponentName: "test",
102 RuntimeInfo: "test",
103 },
104 }
105 conn, err := b.Open(true)
106 if err != nil {
107 t.Fatalf("Could not create in-memory BMDB: %v", err)
108 }
109
110 if err := sc.SSHEquinixEnsure(ctx, f); err != nil {
111 t.Fatalf("Failed to ensure SSH key: %v", err)
112 }
113
114 i.sshClient = &fakeSSHClient{}
115 go i.Run(ctx, conn)
116
117 reservations, _ := f.ListReservations(ctx, sc.ProjectId)
118 kid, err := sc.sshEquinixId(ctx, f)
119 if err != nil {
120 t.Fatalf("Failed to retrieve equinix key ID: %v", err)
121 }
122 sess, err := conn.StartSession(ctx)
123 if err != nil {
124 t.Fatalf("Failed to create BMDB session for verifiaction: %v", err)
125 }
126
127 // Create 10 provided machines for testing.
128 for i := 0; i < 10; i++ {
129 res := reservations[i]
130 dev, _ := f.CreateDevice(ctx, &packngo.DeviceCreateRequest{
131 Hostname: fmt.Sprintf("test-%d", i),
132 OS: "fake",
133 ProjectID: sc.ProjectId,
134 HardwareReservationID: res.ID,
135 ProjectSSHKeys: []string{kid},
136 })
137 f.devices[dev.ID].Network = []*packngo.IPAddressAssignment{
138 {
139 IpAddressCommon: packngo.IpAddressCommon{
140 ID: "fake",
141 Address: "1.2.3.4",
142 Management: true,
143 AddressFamily: 4,
144 Public: true,
145 },
146 },
147 }
148 err = sess.Transact(ctx, func(q *model.Queries) error {
149 machine, err := q.NewMachine(ctx)
150 if err != nil {
151 return err
152 }
153 return q.MachineAddProvided(ctx, model.MachineAddProvidedParams{
154 MachineID: machine.MachineID,
155 Provider: model.ProviderEquinix,
156 ProviderID: dev.ID,
157 })
158 })
159 if err != nil {
160 t.Fatalf("Failed to create BMDB machine: %v", err)
161 }
162 }
163
164 go i.Run(ctx, conn)
165
166 // Expect to find 0 machines needing start.
167 for {
168 time.Sleep(100 * time.Millisecond)
169
170 var machines []model.MachineProvided
171 err = sess.Transact(ctx, func(q *model.Queries) error {
172 var err error
173 machines, err = q.GetMachinesForAgentStart(ctx, 100)
174 return err
175 })
176 if err != nil {
177 t.Fatalf("Failed to run Transaction: %v", err)
178 }
179 if len(machines) == 0 {
180 break
181 }
182 }
183}