blob: b853b82d91f29d5d2b79b501af889fc659bd4303 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Mateusz Zalegafed8fe52022-07-14 16:19:35 +02004package test
5
6import (
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +01007 "bufio"
Mateusz Zalegafed8fe52022-07-14 16:19:35 +02008 "context"
9 "encoding/pem"
10 "fmt"
11 "log"
12 "os"
13 "strings"
14 "testing"
15 "time"
16
Tim Windelschmidt2a1d1b22024-02-06 07:07:42 +010017 "github.com/bazelbuild/rules_go/go/runfiles"
18
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020019 mlaunch "source.monogon.dev/metropolis/test/launch"
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020020 "source.monogon.dev/metropolis/test/util"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020021 "source.monogon.dev/osbase/cmd"
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
Jan Schärb86917b2025-05-14 16:31:08 +000043// This is filled at linking time.
44var metropolisVersion string
45
Mateusz Zalegab838e052022-08-12 18:08:10 +020046// mctlRun starts metroctl, and waits till it exits. It returns nil, if the run
47// was successful.
48func mctlRun(t *testing.T, ctx context.Context, args []string) error {
49 t.Helper()
50
Mateusz Zalegab838e052022-08-12 18:08:10 +020051 log.Printf("$ metroctl %s", strings.Join(args, " "))
52 logf := func(line string) {
53 log.Printf("metroctl: %s", line)
54 }
Tim Windelschmidt82e6af72024-07-23 00:05:42 +000055 _, err := cmd.RunCommand(ctx, xMetroctlPath, args, cmd.WaitUntilCompletion(logf))
Mateusz Zalegab838e052022-08-12 18:08:10 +020056 return err
57}
58
Mateusz Zalegadb75e212022-08-04 17:31:34 +020059// mctlExpectOutput returns true in the event the expected string is found in
60// metroctl output, and false otherwise.
61func mctlExpectOutput(t *testing.T, ctx context.Context, args []string, expect string) (bool, error) {
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020062 t.Helper()
63
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020064 log.Printf("$ metroctl %s", strings.Join(args, " "))
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020065 // Terminate metroctl as soon as the expected output is found.
Mateusz Zalegab838e052022-08-12 18:08:10 +020066 logf := func(line string) {
67 log.Printf("metroctl: %s", line)
68 }
Tim Windelschmidt82e6af72024-07-23 00:05:42 +000069 found, err := cmd.RunCommand(ctx, xMetroctlPath, args, cmd.TerminateIfFound(expect, logf))
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020070 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +020071 return false, fmt.Errorf("while running metroctl: %w", err)
Mateusz Zalegadb75e212022-08-04 17:31:34 +020072 }
73 return found, nil
74}
75
76// mctlFailIfMissing will return a non-nil error value either if the expected
77// output string s is missing in metroctl output, or in case metroctl can't be
78// launched.
79func mctlFailIfMissing(t *testing.T, ctx context.Context, args []string, s string) error {
80 found, err := mctlExpectOutput(t, ctx, args, s)
81 if err != nil {
82 return err
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020083 }
84 if !found {
Mateusz Zalegadb75e212022-08-04 17:31:34 +020085 return fmt.Errorf("expected output is missing: \"%s\"", s)
86 }
87 return nil
88}
89
90// mctlFailIfFound will return a non-nil error value either if the expected
91// output string s is found in metroctl output, or in case metroctl can't be
92// launched.
93func mctlFailIfFound(t *testing.T, ctx context.Context, args []string, s string) error {
94 found, err := mctlExpectOutput(t, ctx, args, s)
95 if err != nil {
96 return err
97 }
98 if found {
99 return fmt.Errorf("unexpected output was found: \"%s\"", s)
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200100 }
101 return nil
102}
103
104func TestMetroctl(t *testing.T) {
105 ctx, ctxC := context.WithCancel(context.Background())
106 defer ctxC()
107
Tim Windelschmidt9f21f532024-05-07 15:14:20 +0200108 co := mlaunch.ClusterOptions{
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200109 NumNodes: 2,
110 }
Tim Windelschmidt9f21f532024-05-07 15:14:20 +0200111 cl, err := mlaunch.LaunchCluster(context.Background(), co)
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200112 if err != nil {
113 t.Fatalf("LaunchCluster failed: %v", err)
114 }
115 defer func() {
116 err := cl.Close()
117 if err != nil {
118 t.Fatalf("cluster Close failed: %v", err)
119 }
120 }()
121
Tim Windelschmidt9f21f532024-05-07 15:14:20 +0200122 socksRemote := fmt.Sprintf("localhost:%d", cl.Ports[mlaunch.SOCKSPort])
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200123 var clusterEndpoints []string
Lorenz Brun3a3c5172023-06-01 19:54:17 +0200124 // Use node starting order for endpoints
125 for _, ep := range cl.NodeIDs {
126 clusterEndpoints = append(clusterEndpoints, cl.Nodes[ep].ManagementAddress)
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200127 }
128
129 ownerPem := pem.EncodeToMemory(&pem.Block{
130 Type: "METROPOLIS INITIAL OWNER PRIVATE KEY",
Tim Windelschmidt9f21f532024-05-07 15:14:20 +0200131 Bytes: mlaunch.InsecurePrivateKey,
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200132 })
133 if err := os.WriteFile("owner-key.pem", ownerPem, 0644); err != nil {
134 log.Fatal("Couldn't write owner-key.pem")
135 }
136
137 commonOpts := []string{
138 "--proxy=" + socksRemote,
139 "--config=.",
Serge Bazanski7eeef0f2024-02-05 14:40:15 +0100140 "--insecure-accept-and-persist-first-encountered-ca",
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200141 }
142
143 var endpointOpts []string
144 for _, ep := range clusterEndpoints {
145 endpointOpts = append(endpointOpts, "--endpoints="+ep)
146 }
147
148 log.Printf("metroctl: Cluster's running, starting tests...")
149 st := t.Run("Init", func(t *testing.T) {
Serge Bazanskibeec27c2024-10-31 12:27:08 +0000150 util.TestEventual(t, "metroctl cluster takeownership", ctx, 30*time.Second, func(ctx context.Context) error {
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200151 // takeownership needs just a single endpoint pointing at the initial node.
152 var args []string
153 args = append(args, commonOpts...)
154 args = append(args, endpointOpts[0])
Serge Bazanskibeec27c2024-10-31 12:27:08 +0000155 args = append(args, "cluster")
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200156 args = append(args, "takeownership")
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200157 return mctlFailIfMissing(t, ctx, args, "Successfully retrieved owner credentials")
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200158 })
159 })
160 if !st {
161 t.Fatalf("metroctl: Couldn't get cluster ownership.")
162 }
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200163 t.Run("list", func(t *testing.T) {
164 util.TestEventual(t, "metroctl list", ctx, 10*time.Second, func(ctx context.Context) error {
165 var args []string
166 args = append(args, commonOpts...)
167 args = append(args, endpointOpts...)
168 args = append(args, "node", "list")
169 // Expect both node IDs to show up in the results.
170 if err := mctlFailIfMissing(t, ctx, args, cl.NodeIDs[0]); err != nil {
171 return err
172 }
173 return mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1])
174 })
175 })
176 t.Run("list [nodeID]", func(t *testing.T) {
177 util.TestEventual(t, "metroctl list [nodeID]", ctx, 10*time.Second, func(ctx context.Context) error {
178 var args []string
179 args = append(args, commonOpts...)
180 args = append(args, endpointOpts...)
181 args = append(args, "node", "list", cl.NodeIDs[1])
182 // Expect just the supplied node IDs to show up in the results.
183 if err := mctlFailIfFound(t, ctx, args, cl.NodeIDs[0]); err != nil {
184 return err
185 }
186 return mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1])
187 })
188 })
189 t.Run("list --output", func(t *testing.T) {
190 util.TestEventual(t, "metroctl list --output", ctx, 10*time.Second, func(ctx context.Context) error {
191 var args []string
192 args = append(args, commonOpts...)
193 args = append(args, endpointOpts...)
194 args = append(args, "node", "list", "--output", "list.txt")
195 // In this case metroctl should write its output to a file rather than stdout.
196 if err := mctlFailIfFound(t, ctx, args, cl.NodeIDs[0]); err != nil {
197 return err
198 }
199 od, err := os.ReadFile("list.txt")
200 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +0200201 return fmt.Errorf("while reading metroctl output file: %w", err)
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200202 }
203 if !strings.Contains(string(od), cl.NodeIDs[0]) {
204 return fmt.Errorf("expected node ID hasn't been found in metroctl output")
205 }
206 return nil
207 })
208 })
209 t.Run("list --filter", func(t *testing.T) {
210 util.TestEventual(t, "metroctl list --filter", ctx, 10*time.Second, func(ctx context.Context) error {
211 nid := cl.NodeIDs[1]
212 naddr := cl.Nodes[nid].ManagementAddress
213
214 var args []string
215 args = append(args, commonOpts...)
216 args = append(args, endpointOpts...)
217 // Filter list results based on nodes' external addresses.
218 args = append(args, "node", "list", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
219 // Expect the second node's ID to show up in the results.
220 if err := mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1]); err != nil {
221 return err
222 }
223 // The first node should've been filtered away.
224 return mctlFailIfFound(t, ctx, args, cl.NodeIDs[0])
225 })
226 })
Mateusz Zalegab838e052022-08-12 18:08:10 +0200227 t.Run("describe --filter", func(t *testing.T) {
228 util.TestEventual(t, "metroctl list --filter", ctx, 10*time.Second, func(ctx context.Context) error {
229 nid := cl.NodeIDs[0]
230 naddr := cl.Nodes[nid].ManagementAddress
231
232 var args []string
233 args = append(args, commonOpts...)
234 args = append(args, endpointOpts...)
235
236 // Filter out the first node. Afterwards, only one node should be left.
237 args = append(args, "node", "describe", "--output", "describe.txt", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
238 if err := mctlRun(t, ctx, args); err != nil {
239 return err
240 }
241
242 // Try matching metroctl output against the advertised format.
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100243 f, err := os.Open("describe.txt")
Mateusz Zalegab838e052022-08-12 18:08:10 +0200244 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +0200245 return fmt.Errorf("while opening metroctl output: %w", err)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200246 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100247 scanner := bufio.NewScanner(f)
248 if !scanner.Scan() {
249 return fmt.Errorf("expected header line")
250 }
251 if !scanner.Scan() {
252 return fmt.Errorf("expected result line")
253 }
254 line := scanner.Text()
255 t.Logf("Line: %q", line)
256
Serge Bazanski0ccc85b2023-11-20 12:59:20 +0100257 var onid, ostate, onaddr, onstatus, onroles, ontpm, onver string
Mateusz Zalegab838e052022-08-12 18:08:10 +0200258 var ontimeout int
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100259
Serge Bazanski0ccc85b2023-11-20 12:59:20 +0100260 _, 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 +0200261 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +0200262 return fmt.Errorf("while parsing metroctl output: %w", err)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200263 }
264 if onid != nid {
265 return fmt.Errorf("node id mismatch")
266 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100267 if ostate != "UP" {
Mateusz Zalegab838e052022-08-12 18:08:10 +0200268 return fmt.Errorf("node state mismatch")
269 }
270 if onaddr != naddr {
271 return fmt.Errorf("node address mismatch")
272 }
273 if onstatus != "HEALTHY" {
274 return fmt.Errorf("node status mismatch")
275 }
Serge Bazanski15f7f632023-03-14 17:17:20 +0100276 if want, got := "ConsensusMember,KubernetesController", onroles; want != got {
277 return fmt.Errorf("node role mismatch: wanted %q, got %q", want, got)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200278 }
Serge Bazanskie4a4ce12023-03-22 18:29:54 +0100279 if want, got := "yes", ontpm; want != got {
280 return fmt.Errorf("node tpm mismatch: wanted %q, got %q", want, got)
281 }
Jan Schärb86917b2025-05-14 16:31:08 +0000282 if want, got := metropolisVersion, onver; want != got {
Serge Bazanski0ccc85b2023-11-20 12:59:20 +0100283 return fmt.Errorf("node version mismatch: wanted %q, got %q", want, got)
284 }
Mateusz Zalegab838e052022-08-12 18:08:10 +0200285 if ontimeout < 0 || ontimeout > 30 {
286 return fmt.Errorf("node timeout mismatch")
287 }
288 return nil
289 })
290 })
Serge Bazanskie012b722023-03-29 17:49:04 +0200291 t.Run("logs [nodeID]", func(t *testing.T) {
292 util.TestEventual(t, "metroctl logs [nodeID]", ctx, 10*time.Second, func(ctx context.Context) error {
293 var args []string
294 args = append(args, commonOpts...)
295 args = append(args, endpointOpts...)
296 args = append(args, "node", "logs", cl.NodeIDs[1])
297
298 if err := mctlFailIfMissing(t, ctx, args, "Cluster enrolment done."); err != nil {
299 return err
300 }
301
302 return nil
303 })
304 })
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200305 t.Run("set/unset role", func(t *testing.T) {
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100306 util.TestEventual(t, "metroctl set/unset role KubernetesWorker", ctx, 10*time.Second, func(ctx context.Context) error {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200307 nid := cl.NodeIDs[1]
308 naddr := cl.Nodes[nid].ManagementAddress
309
310 // In this test we'll unset a node role, make sure that it's been in fact
311 // unset, then set it again, and check again. This exercises commands of
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100312 // the form "metroctl set/unset role KubernetesWorker [NodeID, ...]".
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200313
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100314 // Check that KubernetesWorker role is absent initially.
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200315 var describeArgs []string
316 describeArgs = append(describeArgs, commonOpts...)
317 describeArgs = append(describeArgs, endpointOpts...)
318 describeArgs = append(describeArgs, "node", "describe", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100319 if err := mctlFailIfFound(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200320 return err
321 }
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100322 // Add the role.
323 var setArgs []string
324 setArgs = append(setArgs, commonOpts...)
325 setArgs = append(setArgs, endpointOpts...)
326 setArgs = append(setArgs, "node", "add", "role", "KubernetesWorker", nid)
327 if err := mctlRun(t, ctx, setArgs); err != nil {
328 return err
329 }
330 // Check that the role is set.
331 if err := mctlFailIfMissing(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
332 return err
333 }
334
335 // Remove the role back to the initial value.
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200336 var unsetArgs []string
337 unsetArgs = append(unsetArgs, commonOpts...)
338 unsetArgs = append(unsetArgs, endpointOpts...)
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100339 unsetArgs = append(unsetArgs, "node", "remove", "role", "KubernetesWorker", nid)
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200340 if err := mctlRun(t, ctx, unsetArgs); err != nil {
341 return err
342 }
343 // Check that the role is unset.
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100344 if err := mctlFailIfFound(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200345 return err
346 }
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100347
348 return nil
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200349 })
350 })
Serge Bazanskib701df92024-10-31 14:15:33 +0000351 t.Run("configure", func(t *testing.T) {
352 util.TestEventual(t, "metroctl configure set kubernetes.node_labels_to_synchronize foo bar", ctx, 10*time.Second, func(ctx context.Context) error {
353 var args []string
354 args = append(args, commonOpts...)
355 args = append(args, endpointOpts...)
356 args = append(args, "cluster", "configure", "set", "kubernetes.node_labels_to_synchronize")
357 args = append(args, "foo", "bar")
358
359 if err := mctlFailIfMissing(t, ctx, args, "New value: \"foo\", \"bar\""); err != nil {
360 return err
361 }
362
363 return nil
364 })
365 util.TestEventual(t, "metroctl configure get kubernetes.node_labels_to_synchronize", ctx, 10*time.Second, func(ctx context.Context) error {
366 var args []string
367 args = append(args, commonOpts...)
368 args = append(args, endpointOpts...)
369 args = append(args, "cluster", "configure", "get", "kubernetes.node_labels_to_synchronize")
370
371 if err := mctlFailIfMissing(t, ctx, args, "Value: \"foo\", \"bar\""); err != nil {
372 return err
373 }
374
375 return nil
376 })
377 })
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200378}