blob: 17f5f75bf228c5b97de7f1070c1548f2e9849c7c [file] [log] [blame]
Mateusz Zalegafed8fe52022-07-14 16:19:35 +02001package test
2
3import (
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +01004 "bufio"
Mateusz Zalegafed8fe52022-07-14 16:19:35 +02005 "context"
6 "encoding/pem"
7 "fmt"
8 "log"
9 "os"
10 "strings"
11 "testing"
12 "time"
13
Tim Windelschmidt2a1d1b22024-02-06 07:07:42 +010014 "github.com/bazelbuild/rules_go/go/runfiles"
15
16 mversion "source.monogon.dev/metropolis/version"
17
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020018 mlaunch "source.monogon.dev/metropolis/test/launch"
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020019 "source.monogon.dev/metropolis/test/util"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020020 "source.monogon.dev/osbase/cmd"
Serge Bazanski0ccc85b2023-11-20 12:59:20 +010021 "source.monogon.dev/version"
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020022)
23
Tim Windelschmidt82e6af72024-07-23 00:05:42 +000024var (
25 // These are filled by bazel at linking time with the canonical path of
26 // their corresponding file. Inside the init function we resolve it
27 // with the rules_go runfiles package to the real path.
28 xMetroctlPath string
29)
30
31func init() {
32 var err error
33 for _, path := range []*string{
34 &xMetroctlPath,
35 } {
36 *path, err = runfiles.Rlocation(*path)
37 if err != nil {
38 panic(err)
39 }
Mateusz Zalegab838e052022-08-12 18:08:10 +020040 }
Mateusz Zalegab838e052022-08-12 18:08:10 +020041}
42
43// mctlRun starts metroctl, and waits till it exits. It returns nil, if the run
44// was successful.
45func mctlRun(t *testing.T, ctx context.Context, args []string) error {
46 t.Helper()
47
Mateusz Zalegab838e052022-08-12 18:08:10 +020048 log.Printf("$ metroctl %s", strings.Join(args, " "))
49 logf := func(line string) {
50 log.Printf("metroctl: %s", line)
51 }
Tim Windelschmidt82e6af72024-07-23 00:05:42 +000052 _, err := cmd.RunCommand(ctx, xMetroctlPath, args, cmd.WaitUntilCompletion(logf))
Mateusz Zalegab838e052022-08-12 18:08:10 +020053 return err
54}
55
Mateusz Zalegadb75e212022-08-04 17:31:34 +020056// mctlExpectOutput returns true in the event the expected string is found in
57// metroctl output, and false otherwise.
58func mctlExpectOutput(t *testing.T, ctx context.Context, args []string, expect string) (bool, error) {
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020059 t.Helper()
60
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020061 log.Printf("$ metroctl %s", strings.Join(args, " "))
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020062 // Terminate metroctl as soon as the expected output is found.
Mateusz Zalegab838e052022-08-12 18:08:10 +020063 logf := func(line string) {
64 log.Printf("metroctl: %s", line)
65 }
Tim Windelschmidt82e6af72024-07-23 00:05:42 +000066 found, err := cmd.RunCommand(ctx, xMetroctlPath, args, cmd.TerminateIfFound(expect, logf))
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020067 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +020068 return false, fmt.Errorf("while running metroctl: %w", err)
Mateusz Zalegadb75e212022-08-04 17:31:34 +020069 }
70 return found, nil
71}
72
73// mctlFailIfMissing will return a non-nil error value either if the expected
74// output string s is missing in metroctl output, or in case metroctl can't be
75// launched.
76func mctlFailIfMissing(t *testing.T, ctx context.Context, args []string, s string) error {
77 found, err := mctlExpectOutput(t, ctx, args, s)
78 if err != nil {
79 return err
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020080 }
81 if !found {
Mateusz Zalegadb75e212022-08-04 17:31:34 +020082 return fmt.Errorf("expected output is missing: \"%s\"", s)
83 }
84 return nil
85}
86
87// mctlFailIfFound will return a non-nil error value either if the expected
88// output string s is found in metroctl output, or in case metroctl can't be
89// launched.
90func mctlFailIfFound(t *testing.T, ctx context.Context, args []string, s string) error {
91 found, err := mctlExpectOutput(t, ctx, args, s)
92 if err != nil {
93 return err
94 }
95 if found {
96 return fmt.Errorf("unexpected output was found: \"%s\"", s)
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020097 }
98 return nil
99}
100
101func TestMetroctl(t *testing.T) {
102 ctx, ctxC := context.WithCancel(context.Background())
103 defer ctxC()
104
Tim Windelschmidt9f21f532024-05-07 15:14:20 +0200105 co := mlaunch.ClusterOptions{
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200106 NumNodes: 2,
107 }
Tim Windelschmidt9f21f532024-05-07 15:14:20 +0200108 cl, err := mlaunch.LaunchCluster(context.Background(), co)
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200109 if err != nil {
110 t.Fatalf("LaunchCluster failed: %v", err)
111 }
112 defer func() {
113 err := cl.Close()
114 if err != nil {
115 t.Fatalf("cluster Close failed: %v", err)
116 }
117 }()
118
Tim Windelschmidt9f21f532024-05-07 15:14:20 +0200119 socksRemote := fmt.Sprintf("localhost:%d", cl.Ports[mlaunch.SOCKSPort])
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200120 var clusterEndpoints []string
Lorenz Brun3a3c5172023-06-01 19:54:17 +0200121 // Use node starting order for endpoints
122 for _, ep := range cl.NodeIDs {
123 clusterEndpoints = append(clusterEndpoints, cl.Nodes[ep].ManagementAddress)
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200124 }
125
126 ownerPem := pem.EncodeToMemory(&pem.Block{
127 Type: "METROPOLIS INITIAL OWNER PRIVATE KEY",
Tim Windelschmidt9f21f532024-05-07 15:14:20 +0200128 Bytes: mlaunch.InsecurePrivateKey,
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200129 })
130 if err := os.WriteFile("owner-key.pem", ownerPem, 0644); err != nil {
131 log.Fatal("Couldn't write owner-key.pem")
132 }
133
134 commonOpts := []string{
135 "--proxy=" + socksRemote,
136 "--config=.",
Serge Bazanski7eeef0f2024-02-05 14:40:15 +0100137 "--insecure-accept-and-persist-first-encountered-ca",
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200138 }
139
140 var endpointOpts []string
141 for _, ep := range clusterEndpoints {
142 endpointOpts = append(endpointOpts, "--endpoints="+ep)
143 }
144
145 log.Printf("metroctl: Cluster's running, starting tests...")
146 st := t.Run("Init", func(t *testing.T) {
147 util.TestEventual(t, "metroctl takeownership", ctx, 30*time.Second, func(ctx context.Context) error {
148 // takeownership needs just a single endpoint pointing at the initial node.
149 var args []string
150 args = append(args, commonOpts...)
151 args = append(args, endpointOpts[0])
152 args = append(args, "takeownership")
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200153 return mctlFailIfMissing(t, ctx, args, "Successfully retrieved owner credentials")
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200154 })
155 })
156 if !st {
157 t.Fatalf("metroctl: Couldn't get cluster ownership.")
158 }
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200159 t.Run("list", func(t *testing.T) {
160 util.TestEventual(t, "metroctl list", ctx, 10*time.Second, func(ctx context.Context) error {
161 var args []string
162 args = append(args, commonOpts...)
163 args = append(args, endpointOpts...)
164 args = append(args, "node", "list")
165 // Expect both node IDs to show up in the results.
166 if err := mctlFailIfMissing(t, ctx, args, cl.NodeIDs[0]); err != nil {
167 return err
168 }
169 return mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1])
170 })
171 })
172 t.Run("list [nodeID]", func(t *testing.T) {
173 util.TestEventual(t, "metroctl list [nodeID]", ctx, 10*time.Second, func(ctx context.Context) error {
174 var args []string
175 args = append(args, commonOpts...)
176 args = append(args, endpointOpts...)
177 args = append(args, "node", "list", cl.NodeIDs[1])
178 // Expect just the supplied node IDs to show up in the results.
179 if err := mctlFailIfFound(t, ctx, args, cl.NodeIDs[0]); err != nil {
180 return err
181 }
182 return mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1])
183 })
184 })
185 t.Run("list --output", func(t *testing.T) {
186 util.TestEventual(t, "metroctl list --output", ctx, 10*time.Second, func(ctx context.Context) error {
187 var args []string
188 args = append(args, commonOpts...)
189 args = append(args, endpointOpts...)
190 args = append(args, "node", "list", "--output", "list.txt")
191 // In this case metroctl should write its output to a file rather than stdout.
192 if err := mctlFailIfFound(t, ctx, args, cl.NodeIDs[0]); err != nil {
193 return err
194 }
195 od, err := os.ReadFile("list.txt")
196 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +0200197 return fmt.Errorf("while reading metroctl output file: %w", err)
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200198 }
199 if !strings.Contains(string(od), cl.NodeIDs[0]) {
200 return fmt.Errorf("expected node ID hasn't been found in metroctl output")
201 }
202 return nil
203 })
204 })
205 t.Run("list --filter", func(t *testing.T) {
206 util.TestEventual(t, "metroctl list --filter", ctx, 10*time.Second, func(ctx context.Context) error {
207 nid := cl.NodeIDs[1]
208 naddr := cl.Nodes[nid].ManagementAddress
209
210 var args []string
211 args = append(args, commonOpts...)
212 args = append(args, endpointOpts...)
213 // Filter list results based on nodes' external addresses.
214 args = append(args, "node", "list", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
215 // Expect the second node's ID to show up in the results.
216 if err := mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1]); err != nil {
217 return err
218 }
219 // The first node should've been filtered away.
220 return mctlFailIfFound(t, ctx, args, cl.NodeIDs[0])
221 })
222 })
Mateusz Zalegab838e052022-08-12 18:08:10 +0200223 t.Run("describe --filter", func(t *testing.T) {
224 util.TestEventual(t, "metroctl list --filter", ctx, 10*time.Second, func(ctx context.Context) error {
225 nid := cl.NodeIDs[0]
226 naddr := cl.Nodes[nid].ManagementAddress
227
228 var args []string
229 args = append(args, commonOpts...)
230 args = append(args, endpointOpts...)
231
232 // Filter out the first node. Afterwards, only one node should be left.
233 args = append(args, "node", "describe", "--output", "describe.txt", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
234 if err := mctlRun(t, ctx, args); err != nil {
235 return err
236 }
237
238 // Try matching metroctl output against the advertised format.
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100239 f, err := os.Open("describe.txt")
Mateusz Zalegab838e052022-08-12 18:08:10 +0200240 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +0200241 return fmt.Errorf("while opening metroctl output: %w", err)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200242 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100243 scanner := bufio.NewScanner(f)
244 if !scanner.Scan() {
245 return fmt.Errorf("expected header line")
246 }
247 if !scanner.Scan() {
248 return fmt.Errorf("expected result line")
249 }
250 line := scanner.Text()
251 t.Logf("Line: %q", line)
252
Serge Bazanski0ccc85b2023-11-20 12:59:20 +0100253 var onid, ostate, onaddr, onstatus, onroles, ontpm, onver string
Mateusz Zalegab838e052022-08-12 18:08:10 +0200254 var ontimeout int
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100255
Serge Bazanski0ccc85b2023-11-20 12:59:20 +0100256 _, err = fmt.Sscanf(line, "%s%s%s%s%s%s%s%ds", &onid, &ostate, &onaddr, &onstatus, &onroles, &ontpm, &onver, &ontimeout)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200257 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +0200258 return fmt.Errorf("while parsing metroctl output: %w", err)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200259 }
260 if onid != nid {
261 return fmt.Errorf("node id mismatch")
262 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100263 if ostate != "UP" {
Mateusz Zalegab838e052022-08-12 18:08:10 +0200264 return fmt.Errorf("node state mismatch")
265 }
266 if onaddr != naddr {
267 return fmt.Errorf("node address mismatch")
268 }
269 if onstatus != "HEALTHY" {
270 return fmt.Errorf("node status mismatch")
271 }
Serge Bazanski15f7f632023-03-14 17:17:20 +0100272 if want, got := "ConsensusMember,KubernetesController", onroles; want != got {
273 return fmt.Errorf("node role mismatch: wanted %q, got %q", want, got)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200274 }
Serge Bazanskie4a4ce12023-03-22 18:29:54 +0100275 if want, got := "yes", ontpm; want != got {
276 return fmt.Errorf("node tpm mismatch: wanted %q, got %q", want, got)
277 }
Serge Bazanski0ccc85b2023-11-20 12:59:20 +0100278 if want, got := version.Semver(mversion.Version), onver; want != got {
279 return fmt.Errorf("node version mismatch: wanted %q, got %q", want, got)
280 }
Mateusz Zalegab838e052022-08-12 18:08:10 +0200281 if ontimeout < 0 || ontimeout > 30 {
282 return fmt.Errorf("node timeout mismatch")
283 }
284 return nil
285 })
286 })
Serge Bazanskie012b722023-03-29 17:49:04 +0200287 t.Run("logs [nodeID]", func(t *testing.T) {
288 util.TestEventual(t, "metroctl logs [nodeID]", ctx, 10*time.Second, func(ctx context.Context) error {
289 var args []string
290 args = append(args, commonOpts...)
291 args = append(args, endpointOpts...)
292 args = append(args, "node", "logs", cl.NodeIDs[1])
293
294 if err := mctlFailIfMissing(t, ctx, args, "Cluster enrolment done."); err != nil {
295 return err
296 }
297
298 return nil
299 })
300 })
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200301 t.Run("set/unset role", func(t *testing.T) {
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100302 util.TestEventual(t, "metroctl set/unset role KubernetesWorker", ctx, 10*time.Second, func(ctx context.Context) error {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200303 nid := cl.NodeIDs[1]
304 naddr := cl.Nodes[nid].ManagementAddress
305
306 // In this test we'll unset a node role, make sure that it's been in fact
307 // unset, then set it again, and check again. This exercises commands of
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100308 // the form "metroctl set/unset role KubernetesWorker [NodeID, ...]".
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200309
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100310 // Check that KubernetesWorker role is absent initially.
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200311 var describeArgs []string
312 describeArgs = append(describeArgs, commonOpts...)
313 describeArgs = append(describeArgs, endpointOpts...)
314 describeArgs = append(describeArgs, "node", "describe", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100315 if err := mctlFailIfFound(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200316 return err
317 }
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100318 // Add the role.
319 var setArgs []string
320 setArgs = append(setArgs, commonOpts...)
321 setArgs = append(setArgs, endpointOpts...)
322 setArgs = append(setArgs, "node", "add", "role", "KubernetesWorker", nid)
323 if err := mctlRun(t, ctx, setArgs); err != nil {
324 return err
325 }
326 // Check that the role is set.
327 if err := mctlFailIfMissing(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
328 return err
329 }
330
331 // Remove the role back to the initial value.
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200332 var unsetArgs []string
333 unsetArgs = append(unsetArgs, commonOpts...)
334 unsetArgs = append(unsetArgs, endpointOpts...)
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100335 unsetArgs = append(unsetArgs, "node", "remove", "role", "KubernetesWorker", nid)
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200336 if err := mctlRun(t, ctx, unsetArgs); err != nil {
337 return err
338 }
339 // Check that the role is unset.
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100340 if err := mctlFailIfFound(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200341 return err
342 }
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100343
344 return nil
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200345 })
346 })
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200347}