blob: 064390b7d202dce1093297c2e2d9c5869abcdea8 [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
14 "source.monogon.dev/metropolis/cli/pkg/datafile"
15 "source.monogon.dev/metropolis/pkg/cmd"
16 "source.monogon.dev/metropolis/test/launch/cluster"
17 "source.monogon.dev/metropolis/test/util"
18)
19
Mateusz Zalegab838e052022-08-12 18:08:10 +020020// resolveMetroctl resolves metroctl filesystem path. It will return a correct
21// path, or terminate test execution.
22func resolveMetroctl() string {
23 path, err := datafile.ResolveRunfile("metropolis/cli/metroctl/metroctl_/metroctl")
24 if err != nil {
25 log.Fatalf("Couldn't resolve metroctl binary: %v", err)
26 }
27 return path
28}
29
30// mctlRun starts metroctl, and waits till it exits. It returns nil, if the run
31// was successful.
32func mctlRun(t *testing.T, ctx context.Context, args []string) error {
33 t.Helper()
34
35 path := resolveMetroctl()
36 log.Printf("$ metroctl %s", strings.Join(args, " "))
37 logf := func(line string) {
38 log.Printf("metroctl: %s", line)
39 }
40 _, err := cmd.RunCommand(ctx, path, args, cmd.WaitUntilCompletion(logf))
41 return err
42}
43
Mateusz Zalegadb75e212022-08-04 17:31:34 +020044// mctlExpectOutput returns true in the event the expected string is found in
45// metroctl output, and false otherwise.
46func mctlExpectOutput(t *testing.T, ctx context.Context, args []string, expect string) (bool, error) {
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020047 t.Helper()
48
Mateusz Zalegab838e052022-08-12 18:08:10 +020049 path := resolveMetroctl()
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020050 log.Printf("$ metroctl %s", strings.Join(args, " "))
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020051 // Terminate metroctl as soon as the expected output is found.
Mateusz Zalegab838e052022-08-12 18:08:10 +020052 logf := func(line string) {
53 log.Printf("metroctl: %s", line)
54 }
55 found, err := cmd.RunCommand(ctx, path, args, cmd.TerminateIfFound(expect, logf))
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020056 if err != nil {
Mateusz Zalegadb75e212022-08-04 17:31:34 +020057 return false, fmt.Errorf("while running metroctl: %v", err)
58 }
59 return found, nil
60}
61
62// mctlFailIfMissing will return a non-nil error value either if the expected
63// output string s is missing in metroctl output, or in case metroctl can't be
64// launched.
65func mctlFailIfMissing(t *testing.T, ctx context.Context, args []string, s string) error {
66 found, err := mctlExpectOutput(t, ctx, args, s)
67 if err != nil {
68 return err
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020069 }
70 if !found {
Mateusz Zalegadb75e212022-08-04 17:31:34 +020071 return fmt.Errorf("expected output is missing: \"%s\"", s)
72 }
73 return nil
74}
75
76// mctlFailIfFound will return a non-nil error value either if the expected
77// output string s is found in metroctl output, or in case metroctl can't be
78// launched.
79func mctlFailIfFound(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
83 }
84 if found {
85 return fmt.Errorf("unexpected output was found: \"%s\"", s)
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020086 }
87 return nil
88}
89
90func TestMetroctl(t *testing.T) {
91 ctx, ctxC := context.WithCancel(context.Background())
92 defer ctxC()
93
94 co := cluster.ClusterOptions{
95 NumNodes: 2,
96 }
97 cl, err := cluster.LaunchCluster(context.Background(), co)
98 if err != nil {
99 t.Fatalf("LaunchCluster failed: %v", err)
100 }
101 defer func() {
102 err := cl.Close()
103 if err != nil {
104 t.Fatalf("cluster Close failed: %v", err)
105 }
106 }()
107
108 socksRemote := fmt.Sprintf("localhost:%d", cl.Ports[cluster.SOCKSPort])
109 var clusterEndpoints []string
110 for _, ep := range cl.Nodes {
111 clusterEndpoints = append(clusterEndpoints, ep.ManagementAddress)
112 }
113
114 ownerPem := pem.EncodeToMemory(&pem.Block{
115 Type: "METROPOLIS INITIAL OWNER PRIVATE KEY",
116 Bytes: cluster.InsecurePrivateKey,
117 })
118 if err := os.WriteFile("owner-key.pem", ownerPem, 0644); err != nil {
119 log.Fatal("Couldn't write owner-key.pem")
120 }
121
122 commonOpts := []string{
123 "--proxy=" + socksRemote,
124 "--config=.",
125 }
126
127 var endpointOpts []string
128 for _, ep := range clusterEndpoints {
129 endpointOpts = append(endpointOpts, "--endpoints="+ep)
130 }
131
132 log.Printf("metroctl: Cluster's running, starting tests...")
133 st := t.Run("Init", func(t *testing.T) {
134 util.TestEventual(t, "metroctl takeownership", ctx, 30*time.Second, func(ctx context.Context) error {
135 // takeownership needs just a single endpoint pointing at the initial node.
136 var args []string
137 args = append(args, commonOpts...)
138 args = append(args, endpointOpts[0])
139 args = append(args, "takeownership")
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200140 return mctlFailIfMissing(t, ctx, args, "Successfully retrieved owner credentials")
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200141 })
142 })
143 if !st {
144 t.Fatalf("metroctl: Couldn't get cluster ownership.")
145 }
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200146 t.Run("list", func(t *testing.T) {
147 util.TestEventual(t, "metroctl list", ctx, 10*time.Second, func(ctx context.Context) error {
148 var args []string
149 args = append(args, commonOpts...)
150 args = append(args, endpointOpts...)
151 args = append(args, "node", "list")
152 // Expect both node IDs to show up in the results.
153 if err := mctlFailIfMissing(t, ctx, args, cl.NodeIDs[0]); err != nil {
154 return err
155 }
156 return mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1])
157 })
158 })
159 t.Run("list [nodeID]", func(t *testing.T) {
160 util.TestEventual(t, "metroctl list [nodeID]", 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", cl.NodeIDs[1])
165 // Expect just the supplied node IDs to show up in the results.
166 if err := mctlFailIfFound(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 --output", func(t *testing.T) {
173 util.TestEventual(t, "metroctl list --output", 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", "--output", "list.txt")
178 // In this case metroctl should write its output to a file rather than stdout.
179 if err := mctlFailIfFound(t, ctx, args, cl.NodeIDs[0]); err != nil {
180 return err
181 }
182 od, err := os.ReadFile("list.txt")
183 if err != nil {
184 return fmt.Errorf("while reading metroctl output file: %v", err)
185 }
186 if !strings.Contains(string(od), cl.NodeIDs[0]) {
187 return fmt.Errorf("expected node ID hasn't been found in metroctl output")
188 }
189 return nil
190 })
191 })
192 t.Run("list --filter", func(t *testing.T) {
193 util.TestEventual(t, "metroctl list --filter", ctx, 10*time.Second, func(ctx context.Context) error {
194 nid := cl.NodeIDs[1]
195 naddr := cl.Nodes[nid].ManagementAddress
196
197 var args []string
198 args = append(args, commonOpts...)
199 args = append(args, endpointOpts...)
200 // Filter list results based on nodes' external addresses.
201 args = append(args, "node", "list", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
202 // Expect the second node's ID to show up in the results.
203 if err := mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1]); err != nil {
204 return err
205 }
206 // The first node should've been filtered away.
207 return mctlFailIfFound(t, ctx, args, cl.NodeIDs[0])
208 })
209 })
Mateusz Zalegab838e052022-08-12 18:08:10 +0200210 t.Run("describe --filter", func(t *testing.T) {
211 util.TestEventual(t, "metroctl list --filter", ctx, 10*time.Second, func(ctx context.Context) error {
212 nid := cl.NodeIDs[0]
213 naddr := cl.Nodes[nid].ManagementAddress
214
215 var args []string
216 args = append(args, commonOpts...)
217 args = append(args, endpointOpts...)
218
219 // Filter out the first node. Afterwards, only one node should be left.
220 args = append(args, "node", "describe", "--output", "describe.txt", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
221 if err := mctlRun(t, ctx, args); err != nil {
222 return err
223 }
224
225 // Try matching metroctl output against the advertised format.
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100226 f, err := os.Open("describe.txt")
Mateusz Zalegab838e052022-08-12 18:08:10 +0200227 if err != nil {
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100228 return fmt.Errorf("while opening metroctl output: %v", err)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200229 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100230 scanner := bufio.NewScanner(f)
231 if !scanner.Scan() {
232 return fmt.Errorf("expected header line")
233 }
234 if !scanner.Scan() {
235 return fmt.Errorf("expected result line")
236 }
237 line := scanner.Text()
238 t.Logf("Line: %q", line)
239
Mateusz Zalegab838e052022-08-12 18:08:10 +0200240 var onid, ostate, onaddr, onstatus, onroles string
241 var ontimeout int
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100242
243 _, err = fmt.Sscanf(line, "%s%s%s%s%s%ds", &onid, &ostate, &onaddr, &onstatus, &onroles, &ontimeout)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200244 if err != nil {
245 return fmt.Errorf("while parsing metroctl output: %v", err)
246 }
247 if onid != nid {
248 return fmt.Errorf("node id mismatch")
249 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100250 if ostate != "UP" {
Mateusz Zalegab838e052022-08-12 18:08:10 +0200251 return fmt.Errorf("node state mismatch")
252 }
253 if onaddr != naddr {
254 return fmt.Errorf("node address mismatch")
255 }
256 if onstatus != "HEALTHY" {
257 return fmt.Errorf("node status mismatch")
258 }
Serge Bazanski15f7f632023-03-14 17:17:20 +0100259 if want, got := "ConsensusMember,KubernetesController", onroles; want != got {
260 return fmt.Errorf("node role mismatch: wanted %q, got %q", want, got)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200261 }
262 if ontimeout < 0 || ontimeout > 30 {
263 return fmt.Errorf("node timeout mismatch")
264 }
265 return nil
266 })
267 })
Serge Bazanskie012b722023-03-29 17:49:04 +0200268 t.Run("logs [nodeID]", func(t *testing.T) {
269 util.TestEventual(t, "metroctl logs [nodeID]", ctx, 10*time.Second, func(ctx context.Context) error {
270 var args []string
271 args = append(args, commonOpts...)
272 args = append(args, endpointOpts...)
273 args = append(args, "node", "logs", cl.NodeIDs[1])
274
275 if err := mctlFailIfMissing(t, ctx, args, "Cluster enrolment done."); err != nil {
276 return err
277 }
278
279 return nil
280 })
281 })
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200282 t.Run("set/unset role", func(t *testing.T) {
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100283 util.TestEventual(t, "metroctl set/unset role KubernetesWorker", ctx, 10*time.Second, func(ctx context.Context) error {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200284 nid := cl.NodeIDs[1]
285 naddr := cl.Nodes[nid].ManagementAddress
286
287 // In this test we'll unset a node role, make sure that it's been in fact
288 // unset, then set it again, and check again. This exercises commands of
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100289 // the form "metroctl set/unset role KubernetesWorker [NodeID, ...]".
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200290
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100291 // Check that KubernetesWorker role is absent initially.
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200292 var describeArgs []string
293 describeArgs = append(describeArgs, commonOpts...)
294 describeArgs = append(describeArgs, endpointOpts...)
295 describeArgs = append(describeArgs, "node", "describe", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100296 if err := mctlFailIfFound(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200297 return err
298 }
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100299 // Add the role.
300 var setArgs []string
301 setArgs = append(setArgs, commonOpts...)
302 setArgs = append(setArgs, endpointOpts...)
303 setArgs = append(setArgs, "node", "add", "role", "KubernetesWorker", nid)
304 if err := mctlRun(t, ctx, setArgs); err != nil {
305 return err
306 }
307 // Check that the role is set.
308 if err := mctlFailIfMissing(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
309 return err
310 }
311
312 // Remove the role back to the initial value.
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200313 var unsetArgs []string
314 unsetArgs = append(unsetArgs, commonOpts...)
315 unsetArgs = append(unsetArgs, endpointOpts...)
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100316 unsetArgs = append(unsetArgs, "node", "remove", "role", "KubernetesWorker", nid)
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200317 if err := mctlRun(t, ctx, unsetArgs); err != nil {
318 return err
319 }
320 // Check that the role is unset.
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100321 if err := mctlFailIfFound(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200322 return err
323 }
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100324
325 return nil
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200326 })
327 })
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200328}