blob: 0145cfe04829190a6d8514332512b1db88d5a0a3 [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
Lorenz Brun3a3c5172023-06-01 19:54:17 +0200110 // Use node starting order for endpoints
111 for _, ep := range cl.NodeIDs {
112 clusterEndpoints = append(clusterEndpoints, cl.Nodes[ep].ManagementAddress)
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200113 }
114
115 ownerPem := pem.EncodeToMemory(&pem.Block{
116 Type: "METROPOLIS INITIAL OWNER PRIVATE KEY",
117 Bytes: cluster.InsecurePrivateKey,
118 })
119 if err := os.WriteFile("owner-key.pem", ownerPem, 0644); err != nil {
120 log.Fatal("Couldn't write owner-key.pem")
121 }
122
123 commonOpts := []string{
124 "--proxy=" + socksRemote,
125 "--config=.",
126 }
127
128 var endpointOpts []string
129 for _, ep := range clusterEndpoints {
130 endpointOpts = append(endpointOpts, "--endpoints="+ep)
131 }
132
133 log.Printf("metroctl: Cluster's running, starting tests...")
134 st := t.Run("Init", func(t *testing.T) {
135 util.TestEventual(t, "metroctl takeownership", ctx, 30*time.Second, func(ctx context.Context) error {
136 // takeownership needs just a single endpoint pointing at the initial node.
137 var args []string
138 args = append(args, commonOpts...)
139 args = append(args, endpointOpts[0])
140 args = append(args, "takeownership")
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200141 return mctlFailIfMissing(t, ctx, args, "Successfully retrieved owner credentials")
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200142 })
143 })
144 if !st {
145 t.Fatalf("metroctl: Couldn't get cluster ownership.")
146 }
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200147 t.Run("list", func(t *testing.T) {
148 util.TestEventual(t, "metroctl list", ctx, 10*time.Second, func(ctx context.Context) error {
149 var args []string
150 args = append(args, commonOpts...)
151 args = append(args, endpointOpts...)
152 args = append(args, "node", "list")
153 // Expect both node IDs to show up in the results.
154 if err := mctlFailIfMissing(t, ctx, args, cl.NodeIDs[0]); err != nil {
155 return err
156 }
157 return mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1])
158 })
159 })
160 t.Run("list [nodeID]", func(t *testing.T) {
161 util.TestEventual(t, "metroctl list [nodeID]", 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", cl.NodeIDs[1])
166 // Expect just the supplied node IDs to show up in the results.
167 if err := mctlFailIfFound(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 --output", func(t *testing.T) {
174 util.TestEventual(t, "metroctl list --output", 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", "--output", "list.txt")
179 // In this case metroctl should write its output to a file rather than stdout.
180 if err := mctlFailIfFound(t, ctx, args, cl.NodeIDs[0]); err != nil {
181 return err
182 }
183 od, err := os.ReadFile("list.txt")
184 if err != nil {
185 return fmt.Errorf("while reading metroctl output file: %v", err)
186 }
187 if !strings.Contains(string(od), cl.NodeIDs[0]) {
188 return fmt.Errorf("expected node ID hasn't been found in metroctl output")
189 }
190 return nil
191 })
192 })
193 t.Run("list --filter", func(t *testing.T) {
194 util.TestEventual(t, "metroctl list --filter", ctx, 10*time.Second, func(ctx context.Context) error {
195 nid := cl.NodeIDs[1]
196 naddr := cl.Nodes[nid].ManagementAddress
197
198 var args []string
199 args = append(args, commonOpts...)
200 args = append(args, endpointOpts...)
201 // Filter list results based on nodes' external addresses.
202 args = append(args, "node", "list", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
203 // Expect the second node's ID to show up in the results.
204 if err := mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1]); err != nil {
205 return err
206 }
207 // The first node should've been filtered away.
208 return mctlFailIfFound(t, ctx, args, cl.NodeIDs[0])
209 })
210 })
Mateusz Zalegab838e052022-08-12 18:08:10 +0200211 t.Run("describe --filter", func(t *testing.T) {
212 util.TestEventual(t, "metroctl list --filter", ctx, 10*time.Second, func(ctx context.Context) error {
213 nid := cl.NodeIDs[0]
214 naddr := cl.Nodes[nid].ManagementAddress
215
216 var args []string
217 args = append(args, commonOpts...)
218 args = append(args, endpointOpts...)
219
220 // Filter out the first node. Afterwards, only one node should be left.
221 args = append(args, "node", "describe", "--output", "describe.txt", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
222 if err := mctlRun(t, ctx, args); err != nil {
223 return err
224 }
225
226 // Try matching metroctl output against the advertised format.
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100227 f, err := os.Open("describe.txt")
Mateusz Zalegab838e052022-08-12 18:08:10 +0200228 if err != nil {
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100229 return fmt.Errorf("while opening metroctl output: %v", err)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200230 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100231 scanner := bufio.NewScanner(f)
232 if !scanner.Scan() {
233 return fmt.Errorf("expected header line")
234 }
235 if !scanner.Scan() {
236 return fmt.Errorf("expected result line")
237 }
238 line := scanner.Text()
239 t.Logf("Line: %q", line)
240
Serge Bazanskie4a4ce12023-03-22 18:29:54 +0100241 var onid, ostate, onaddr, onstatus, onroles, ontpm string
Mateusz Zalegab838e052022-08-12 18:08:10 +0200242 var ontimeout int
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100243
Serge Bazanskie4a4ce12023-03-22 18:29:54 +0100244 _, err = fmt.Sscanf(line, "%s%s%s%s%s%s%ds", &onid, &ostate, &onaddr, &onstatus, &onroles, &ontpm, &ontimeout)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200245 if err != nil {
246 return fmt.Errorf("while parsing metroctl output: %v", err)
247 }
248 if onid != nid {
249 return fmt.Errorf("node id mismatch")
250 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100251 if ostate != "UP" {
Mateusz Zalegab838e052022-08-12 18:08:10 +0200252 return fmt.Errorf("node state mismatch")
253 }
254 if onaddr != naddr {
255 return fmt.Errorf("node address mismatch")
256 }
257 if onstatus != "HEALTHY" {
258 return fmt.Errorf("node status mismatch")
259 }
Serge Bazanski15f7f632023-03-14 17:17:20 +0100260 if want, got := "ConsensusMember,KubernetesController", onroles; want != got {
261 return fmt.Errorf("node role mismatch: wanted %q, got %q", want, got)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200262 }
Serge Bazanskie4a4ce12023-03-22 18:29:54 +0100263 if want, got := "yes", ontpm; want != got {
264 return fmt.Errorf("node tpm mismatch: wanted %q, got %q", want, got)
265 }
Mateusz Zalegab838e052022-08-12 18:08:10 +0200266 if ontimeout < 0 || ontimeout > 30 {
267 return fmt.Errorf("node timeout mismatch")
268 }
269 return nil
270 })
271 })
Serge Bazanskie012b722023-03-29 17:49:04 +0200272 t.Run("logs [nodeID]", func(t *testing.T) {
273 util.TestEventual(t, "metroctl logs [nodeID]", ctx, 10*time.Second, func(ctx context.Context) error {
274 var args []string
275 args = append(args, commonOpts...)
276 args = append(args, endpointOpts...)
277 args = append(args, "node", "logs", cl.NodeIDs[1])
278
279 if err := mctlFailIfMissing(t, ctx, args, "Cluster enrolment done."); err != nil {
280 return err
281 }
282
283 return nil
284 })
285 })
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200286 t.Run("set/unset role", func(t *testing.T) {
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100287 util.TestEventual(t, "metroctl set/unset role KubernetesWorker", ctx, 10*time.Second, func(ctx context.Context) error {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200288 nid := cl.NodeIDs[1]
289 naddr := cl.Nodes[nid].ManagementAddress
290
291 // In this test we'll unset a node role, make sure that it's been in fact
292 // unset, then set it again, and check again. This exercises commands of
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100293 // the form "metroctl set/unset role KubernetesWorker [NodeID, ...]".
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200294
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100295 // Check that KubernetesWorker role is absent initially.
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200296 var describeArgs []string
297 describeArgs = append(describeArgs, commonOpts...)
298 describeArgs = append(describeArgs, endpointOpts...)
299 describeArgs = append(describeArgs, "node", "describe", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100300 if err := mctlFailIfFound(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200301 return err
302 }
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100303 // Add the role.
304 var setArgs []string
305 setArgs = append(setArgs, commonOpts...)
306 setArgs = append(setArgs, endpointOpts...)
307 setArgs = append(setArgs, "node", "add", "role", "KubernetesWorker", nid)
308 if err := mctlRun(t, ctx, setArgs); err != nil {
309 return err
310 }
311 // Check that the role is set.
312 if err := mctlFailIfMissing(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
313 return err
314 }
315
316 // Remove the role back to the initial value.
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200317 var unsetArgs []string
318 unsetArgs = append(unsetArgs, commonOpts...)
319 unsetArgs = append(unsetArgs, endpointOpts...)
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100320 unsetArgs = append(unsetArgs, "node", "remove", "role", "KubernetesWorker", nid)
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200321 if err := mctlRun(t, ctx, unsetArgs); err != nil {
322 return err
323 }
324 // Check that the role is unset.
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100325 if err := mctlFailIfFound(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200326 return err
327 }
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100328
329 return nil
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200330 })
331 })
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200332}