blob: f4bba02fcf377a137ec37485ea246ae5ca912253 [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) {
Serge Bazanskibeec27c2024-10-31 12:27:08 +0000147 util.TestEventual(t, "metroctl cluster takeownership", ctx, 30*time.Second, func(ctx context.Context) error {
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200148 // 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])
Serge Bazanskibeec27c2024-10-31 12:27:08 +0000152 args = append(args, "cluster")
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200153 args = append(args, "takeownership")
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200154 return mctlFailIfMissing(t, ctx, args, "Successfully retrieved owner credentials")
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200155 })
156 })
157 if !st {
158 t.Fatalf("metroctl: Couldn't get cluster ownership.")
159 }
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200160 t.Run("list", func(t *testing.T) {
161 util.TestEventual(t, "metroctl list", ctx, 10*time.Second, func(ctx context.Context) error {
162 var args []string
163 args = append(args, commonOpts...)
164 args = append(args, endpointOpts...)
165 args = append(args, "node", "list")
166 // Expect both node IDs to show up in the results.
167 if err := mctlFailIfMissing(t, ctx, args, cl.NodeIDs[0]); err != nil {
168 return err
169 }
170 return mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1])
171 })
172 })
173 t.Run("list [nodeID]", func(t *testing.T) {
174 util.TestEventual(t, "metroctl list [nodeID]", ctx, 10*time.Second, func(ctx context.Context) error {
175 var args []string
176 args = append(args, commonOpts...)
177 args = append(args, endpointOpts...)
178 args = append(args, "node", "list", cl.NodeIDs[1])
179 // Expect just the supplied node IDs to show up in the results.
180 if err := mctlFailIfFound(t, ctx, args, cl.NodeIDs[0]); err != nil {
181 return err
182 }
183 return mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1])
184 })
185 })
186 t.Run("list --output", func(t *testing.T) {
187 util.TestEventual(t, "metroctl list --output", ctx, 10*time.Second, func(ctx context.Context) error {
188 var args []string
189 args = append(args, commonOpts...)
190 args = append(args, endpointOpts...)
191 args = append(args, "node", "list", "--output", "list.txt")
192 // In this case metroctl should write its output to a file rather than stdout.
193 if err := mctlFailIfFound(t, ctx, args, cl.NodeIDs[0]); err != nil {
194 return err
195 }
196 od, err := os.ReadFile("list.txt")
197 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +0200198 return fmt.Errorf("while reading metroctl output file: %w", err)
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200199 }
200 if !strings.Contains(string(od), cl.NodeIDs[0]) {
201 return fmt.Errorf("expected node ID hasn't been found in metroctl output")
202 }
203 return nil
204 })
205 })
206 t.Run("list --filter", func(t *testing.T) {
207 util.TestEventual(t, "metroctl list --filter", ctx, 10*time.Second, func(ctx context.Context) error {
208 nid := cl.NodeIDs[1]
209 naddr := cl.Nodes[nid].ManagementAddress
210
211 var args []string
212 args = append(args, commonOpts...)
213 args = append(args, endpointOpts...)
214 // Filter list results based on nodes' external addresses.
215 args = append(args, "node", "list", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
216 // Expect the second node's ID to show up in the results.
217 if err := mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1]); err != nil {
218 return err
219 }
220 // The first node should've been filtered away.
221 return mctlFailIfFound(t, ctx, args, cl.NodeIDs[0])
222 })
223 })
Mateusz Zalegab838e052022-08-12 18:08:10 +0200224 t.Run("describe --filter", func(t *testing.T) {
225 util.TestEventual(t, "metroctl list --filter", ctx, 10*time.Second, func(ctx context.Context) error {
226 nid := cl.NodeIDs[0]
227 naddr := cl.Nodes[nid].ManagementAddress
228
229 var args []string
230 args = append(args, commonOpts...)
231 args = append(args, endpointOpts...)
232
233 // Filter out the first node. Afterwards, only one node should be left.
234 args = append(args, "node", "describe", "--output", "describe.txt", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
235 if err := mctlRun(t, ctx, args); err != nil {
236 return err
237 }
238
239 // Try matching metroctl output against the advertised format.
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100240 f, err := os.Open("describe.txt")
Mateusz Zalegab838e052022-08-12 18:08:10 +0200241 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +0200242 return fmt.Errorf("while opening metroctl output: %w", err)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200243 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100244 scanner := bufio.NewScanner(f)
245 if !scanner.Scan() {
246 return fmt.Errorf("expected header line")
247 }
248 if !scanner.Scan() {
249 return fmt.Errorf("expected result line")
250 }
251 line := scanner.Text()
252 t.Logf("Line: %q", line)
253
Serge Bazanski0ccc85b2023-11-20 12:59:20 +0100254 var onid, ostate, onaddr, onstatus, onroles, ontpm, onver string
Mateusz Zalegab838e052022-08-12 18:08:10 +0200255 var ontimeout int
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100256
Serge Bazanski0ccc85b2023-11-20 12:59:20 +0100257 _, 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 +0200258 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +0200259 return fmt.Errorf("while parsing metroctl output: %w", err)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200260 }
261 if onid != nid {
262 return fmt.Errorf("node id mismatch")
263 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100264 if ostate != "UP" {
Mateusz Zalegab838e052022-08-12 18:08:10 +0200265 return fmt.Errorf("node state mismatch")
266 }
267 if onaddr != naddr {
268 return fmt.Errorf("node address mismatch")
269 }
270 if onstatus != "HEALTHY" {
271 return fmt.Errorf("node status mismatch")
272 }
Serge Bazanski15f7f632023-03-14 17:17:20 +0100273 if want, got := "ConsensusMember,KubernetesController", onroles; want != got {
274 return fmt.Errorf("node role mismatch: wanted %q, got %q", want, got)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200275 }
Serge Bazanskie4a4ce12023-03-22 18:29:54 +0100276 if want, got := "yes", ontpm; want != got {
277 return fmt.Errorf("node tpm mismatch: wanted %q, got %q", want, got)
278 }
Serge Bazanski0ccc85b2023-11-20 12:59:20 +0100279 if want, got := version.Semver(mversion.Version), onver; want != got {
280 return fmt.Errorf("node version mismatch: wanted %q, got %q", want, got)
281 }
Mateusz Zalegab838e052022-08-12 18:08:10 +0200282 if ontimeout < 0 || ontimeout > 30 {
283 return fmt.Errorf("node timeout mismatch")
284 }
285 return nil
286 })
287 })
Serge Bazanskie012b722023-03-29 17:49:04 +0200288 t.Run("logs [nodeID]", func(t *testing.T) {
289 util.TestEventual(t, "metroctl logs [nodeID]", ctx, 10*time.Second, func(ctx context.Context) error {
290 var args []string
291 args = append(args, commonOpts...)
292 args = append(args, endpointOpts...)
293 args = append(args, "node", "logs", cl.NodeIDs[1])
294
295 if err := mctlFailIfMissing(t, ctx, args, "Cluster enrolment done."); err != nil {
296 return err
297 }
298
299 return nil
300 })
301 })
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200302 t.Run("set/unset role", func(t *testing.T) {
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100303 util.TestEventual(t, "metroctl set/unset role KubernetesWorker", ctx, 10*time.Second, func(ctx context.Context) error {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200304 nid := cl.NodeIDs[1]
305 naddr := cl.Nodes[nid].ManagementAddress
306
307 // In this test we'll unset a node role, make sure that it's been in fact
308 // unset, then set it again, and check again. This exercises commands of
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100309 // the form "metroctl set/unset role KubernetesWorker [NodeID, ...]".
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200310
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100311 // Check that KubernetesWorker role is absent initially.
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200312 var describeArgs []string
313 describeArgs = append(describeArgs, commonOpts...)
314 describeArgs = append(describeArgs, endpointOpts...)
315 describeArgs = append(describeArgs, "node", "describe", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100316 if err := mctlFailIfFound(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200317 return err
318 }
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100319 // Add the role.
320 var setArgs []string
321 setArgs = append(setArgs, commonOpts...)
322 setArgs = append(setArgs, endpointOpts...)
323 setArgs = append(setArgs, "node", "add", "role", "KubernetesWorker", nid)
324 if err := mctlRun(t, ctx, setArgs); err != nil {
325 return err
326 }
327 // Check that the role is set.
328 if err := mctlFailIfMissing(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
329 return err
330 }
331
332 // Remove the role back to the initial value.
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200333 var unsetArgs []string
334 unsetArgs = append(unsetArgs, commonOpts...)
335 unsetArgs = append(unsetArgs, endpointOpts...)
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100336 unsetArgs = append(unsetArgs, "node", "remove", "role", "KubernetesWorker", nid)
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200337 if err := mctlRun(t, ctx, unsetArgs); err != nil {
338 return err
339 }
340 // Check that the role is unset.
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100341 if err := mctlFailIfFound(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200342 return err
343 }
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100344
345 return nil
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200346 })
347 })
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200348}