blob: 969b4cc58a8e00e94530fcda4a5421ce9b03d18b [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"
Serge Bazanski0ccc85b2023-11-20 12:59:20 +010018 mversion "source.monogon.dev/metropolis/version"
19 "source.monogon.dev/version"
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020020)
21
Mateusz Zalegab838e052022-08-12 18:08:10 +020022// resolveMetroctl resolves metroctl filesystem path. It will return a correct
23// path, or terminate test execution.
24func resolveMetroctl() string {
25 path, err := datafile.ResolveRunfile("metropolis/cli/metroctl/metroctl_/metroctl")
26 if err != nil {
27 log.Fatalf("Couldn't resolve metroctl binary: %v", err)
28 }
29 return path
30}
31
32// mctlRun starts metroctl, and waits till it exits. It returns nil, if the run
33// was successful.
34func mctlRun(t *testing.T, ctx context.Context, args []string) error {
35 t.Helper()
36
37 path := resolveMetroctl()
38 log.Printf("$ metroctl %s", strings.Join(args, " "))
39 logf := func(line string) {
40 log.Printf("metroctl: %s", line)
41 }
42 _, err := cmd.RunCommand(ctx, path, args, cmd.WaitUntilCompletion(logf))
43 return err
44}
45
Mateusz Zalegadb75e212022-08-04 17:31:34 +020046// mctlExpectOutput returns true in the event the expected string is found in
47// metroctl output, and false otherwise.
48func mctlExpectOutput(t *testing.T, ctx context.Context, args []string, expect string) (bool, error) {
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020049 t.Helper()
50
Mateusz Zalegab838e052022-08-12 18:08:10 +020051 path := resolveMetroctl()
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020052 log.Printf("$ metroctl %s", strings.Join(args, " "))
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020053 // Terminate metroctl as soon as the expected output is found.
Mateusz Zalegab838e052022-08-12 18:08:10 +020054 logf := func(line string) {
55 log.Printf("metroctl: %s", line)
56 }
57 found, err := cmd.RunCommand(ctx, path, args, cmd.TerminateIfFound(expect, logf))
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020058 if err != nil {
Mateusz Zalegadb75e212022-08-04 17:31:34 +020059 return false, fmt.Errorf("while running metroctl: %v", err)
60 }
61 return found, nil
62}
63
64// mctlFailIfMissing will return a non-nil error value either if the expected
65// output string s is missing in metroctl output, or in case metroctl can't be
66// launched.
67func mctlFailIfMissing(t *testing.T, ctx context.Context, args []string, s string) error {
68 found, err := mctlExpectOutput(t, ctx, args, s)
69 if err != nil {
70 return err
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020071 }
72 if !found {
Mateusz Zalegadb75e212022-08-04 17:31:34 +020073 return fmt.Errorf("expected output is missing: \"%s\"", s)
74 }
75 return nil
76}
77
78// mctlFailIfFound will return a non-nil error value either if the expected
79// output string s is found in metroctl output, or in case metroctl can't be
80// launched.
81func mctlFailIfFound(t *testing.T, ctx context.Context, args []string, s string) error {
82 found, err := mctlExpectOutput(t, ctx, args, s)
83 if err != nil {
84 return err
85 }
86 if found {
87 return fmt.Errorf("unexpected output was found: \"%s\"", s)
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020088 }
89 return nil
90}
91
92func TestMetroctl(t *testing.T) {
93 ctx, ctxC := context.WithCancel(context.Background())
94 defer ctxC()
95
96 co := cluster.ClusterOptions{
97 NumNodes: 2,
98 }
99 cl, err := cluster.LaunchCluster(context.Background(), co)
100 if err != nil {
101 t.Fatalf("LaunchCluster failed: %v", err)
102 }
103 defer func() {
104 err := cl.Close()
105 if err != nil {
106 t.Fatalf("cluster Close failed: %v", err)
107 }
108 }()
109
110 socksRemote := fmt.Sprintf("localhost:%d", cl.Ports[cluster.SOCKSPort])
111 var clusterEndpoints []string
Lorenz Brun3a3c5172023-06-01 19:54:17 +0200112 // Use node starting order for endpoints
113 for _, ep := range cl.NodeIDs {
114 clusterEndpoints = append(clusterEndpoints, cl.Nodes[ep].ManagementAddress)
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200115 }
116
117 ownerPem := pem.EncodeToMemory(&pem.Block{
118 Type: "METROPOLIS INITIAL OWNER PRIVATE KEY",
119 Bytes: cluster.InsecurePrivateKey,
120 })
121 if err := os.WriteFile("owner-key.pem", ownerPem, 0644); err != nil {
122 log.Fatal("Couldn't write owner-key.pem")
123 }
124
125 commonOpts := []string{
126 "--proxy=" + socksRemote,
127 "--config=.",
128 }
129
130 var endpointOpts []string
131 for _, ep := range clusterEndpoints {
132 endpointOpts = append(endpointOpts, "--endpoints="+ep)
133 }
134
135 log.Printf("metroctl: Cluster's running, starting tests...")
136 st := t.Run("Init", func(t *testing.T) {
137 util.TestEventual(t, "metroctl takeownership", ctx, 30*time.Second, func(ctx context.Context) error {
138 // takeownership needs just a single endpoint pointing at the initial node.
139 var args []string
140 args = append(args, commonOpts...)
141 args = append(args, endpointOpts[0])
142 args = append(args, "takeownership")
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200143 return mctlFailIfMissing(t, ctx, args, "Successfully retrieved owner credentials")
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200144 })
145 })
146 if !st {
147 t.Fatalf("metroctl: Couldn't get cluster ownership.")
148 }
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200149 t.Run("list", func(t *testing.T) {
150 util.TestEventual(t, "metroctl list", ctx, 10*time.Second, func(ctx context.Context) error {
151 var args []string
152 args = append(args, commonOpts...)
153 args = append(args, endpointOpts...)
154 args = append(args, "node", "list")
155 // Expect both node IDs to show up in the results.
156 if err := mctlFailIfMissing(t, ctx, args, cl.NodeIDs[0]); err != nil {
157 return err
158 }
159 return mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1])
160 })
161 })
162 t.Run("list [nodeID]", func(t *testing.T) {
163 util.TestEventual(t, "metroctl list [nodeID]", ctx, 10*time.Second, func(ctx context.Context) error {
164 var args []string
165 args = append(args, commonOpts...)
166 args = append(args, endpointOpts...)
167 args = append(args, "node", "list", cl.NodeIDs[1])
168 // Expect just the supplied node IDs to show up in the results.
169 if err := mctlFailIfFound(t, ctx, args, cl.NodeIDs[0]); err != nil {
170 return err
171 }
172 return mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1])
173 })
174 })
175 t.Run("list --output", func(t *testing.T) {
176 util.TestEventual(t, "metroctl list --output", ctx, 10*time.Second, func(ctx context.Context) error {
177 var args []string
178 args = append(args, commonOpts...)
179 args = append(args, endpointOpts...)
180 args = append(args, "node", "list", "--output", "list.txt")
181 // In this case metroctl should write its output to a file rather than stdout.
182 if err := mctlFailIfFound(t, ctx, args, cl.NodeIDs[0]); err != nil {
183 return err
184 }
185 od, err := os.ReadFile("list.txt")
186 if err != nil {
187 return fmt.Errorf("while reading metroctl output file: %v", err)
188 }
189 if !strings.Contains(string(od), cl.NodeIDs[0]) {
190 return fmt.Errorf("expected node ID hasn't been found in metroctl output")
191 }
192 return nil
193 })
194 })
195 t.Run("list --filter", func(t *testing.T) {
196 util.TestEventual(t, "metroctl list --filter", ctx, 10*time.Second, func(ctx context.Context) error {
197 nid := cl.NodeIDs[1]
198 naddr := cl.Nodes[nid].ManagementAddress
199
200 var args []string
201 args = append(args, commonOpts...)
202 args = append(args, endpointOpts...)
203 // Filter list results based on nodes' external addresses.
204 args = append(args, "node", "list", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
205 // Expect the second node's ID to show up in the results.
206 if err := mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1]); err != nil {
207 return err
208 }
209 // The first node should've been filtered away.
210 return mctlFailIfFound(t, ctx, args, cl.NodeIDs[0])
211 })
212 })
Mateusz Zalegab838e052022-08-12 18:08:10 +0200213 t.Run("describe --filter", func(t *testing.T) {
214 util.TestEventual(t, "metroctl list --filter", ctx, 10*time.Second, func(ctx context.Context) error {
215 nid := cl.NodeIDs[0]
216 naddr := cl.Nodes[nid].ManagementAddress
217
218 var args []string
219 args = append(args, commonOpts...)
220 args = append(args, endpointOpts...)
221
222 // Filter out the first node. Afterwards, only one node should be left.
223 args = append(args, "node", "describe", "--output", "describe.txt", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
224 if err := mctlRun(t, ctx, args); err != nil {
225 return err
226 }
227
228 // Try matching metroctl output against the advertised format.
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100229 f, err := os.Open("describe.txt")
Mateusz Zalegab838e052022-08-12 18:08:10 +0200230 if err != nil {
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100231 return fmt.Errorf("while opening metroctl output: %v", err)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200232 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100233 scanner := bufio.NewScanner(f)
234 if !scanner.Scan() {
235 return fmt.Errorf("expected header line")
236 }
237 if !scanner.Scan() {
238 return fmt.Errorf("expected result line")
239 }
240 line := scanner.Text()
241 t.Logf("Line: %q", line)
242
Serge Bazanski0ccc85b2023-11-20 12:59:20 +0100243 var onid, ostate, onaddr, onstatus, onroles, ontpm, onver string
Mateusz Zalegab838e052022-08-12 18:08:10 +0200244 var ontimeout int
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100245
Serge Bazanski0ccc85b2023-11-20 12:59:20 +0100246 _, 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 +0200247 if err != nil {
248 return fmt.Errorf("while parsing metroctl output: %v", err)
249 }
250 if onid != nid {
251 return fmt.Errorf("node id mismatch")
252 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100253 if ostate != "UP" {
Mateusz Zalegab838e052022-08-12 18:08:10 +0200254 return fmt.Errorf("node state mismatch")
255 }
256 if onaddr != naddr {
257 return fmt.Errorf("node address mismatch")
258 }
259 if onstatus != "HEALTHY" {
260 return fmt.Errorf("node status mismatch")
261 }
Serge Bazanski15f7f632023-03-14 17:17:20 +0100262 if want, got := "ConsensusMember,KubernetesController", onroles; want != got {
263 return fmt.Errorf("node role mismatch: wanted %q, got %q", want, got)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200264 }
Serge Bazanskie4a4ce12023-03-22 18:29:54 +0100265 if want, got := "yes", ontpm; want != got {
266 return fmt.Errorf("node tpm mismatch: wanted %q, got %q", want, got)
267 }
Serge Bazanski0ccc85b2023-11-20 12:59:20 +0100268 if want, got := version.Semver(mversion.Version), onver; want != got {
269 return fmt.Errorf("node version mismatch: wanted %q, got %q", want, got)
270 }
Mateusz Zalegab838e052022-08-12 18:08:10 +0200271 if ontimeout < 0 || ontimeout > 30 {
272 return fmt.Errorf("node timeout mismatch")
273 }
274 return nil
275 })
276 })
Serge Bazanskie012b722023-03-29 17:49:04 +0200277 t.Run("logs [nodeID]", func(t *testing.T) {
278 util.TestEventual(t, "metroctl logs [nodeID]", ctx, 10*time.Second, func(ctx context.Context) error {
279 var args []string
280 args = append(args, commonOpts...)
281 args = append(args, endpointOpts...)
282 args = append(args, "node", "logs", cl.NodeIDs[1])
283
284 if err := mctlFailIfMissing(t, ctx, args, "Cluster enrolment done."); err != nil {
285 return err
286 }
287
288 return nil
289 })
290 })
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200291 t.Run("set/unset role", func(t *testing.T) {
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100292 util.TestEventual(t, "metroctl set/unset role KubernetesWorker", ctx, 10*time.Second, func(ctx context.Context) error {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200293 nid := cl.NodeIDs[1]
294 naddr := cl.Nodes[nid].ManagementAddress
295
296 // In this test we'll unset a node role, make sure that it's been in fact
297 // unset, then set it again, and check again. This exercises commands of
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100298 // the form "metroctl set/unset role KubernetesWorker [NodeID, ...]".
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200299
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100300 // Check that KubernetesWorker role is absent initially.
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200301 var describeArgs []string
302 describeArgs = append(describeArgs, commonOpts...)
303 describeArgs = append(describeArgs, endpointOpts...)
304 describeArgs = append(describeArgs, "node", "describe", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100305 if err := mctlFailIfFound(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200306 return err
307 }
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100308 // Add the role.
309 var setArgs []string
310 setArgs = append(setArgs, commonOpts...)
311 setArgs = append(setArgs, endpointOpts...)
312 setArgs = append(setArgs, "node", "add", "role", "KubernetesWorker", nid)
313 if err := mctlRun(t, ctx, setArgs); err != nil {
314 return err
315 }
316 // Check that the role is set.
317 if err := mctlFailIfMissing(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
318 return err
319 }
320
321 // Remove the role back to the initial value.
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200322 var unsetArgs []string
323 unsetArgs = append(unsetArgs, commonOpts...)
324 unsetArgs = append(unsetArgs, endpointOpts...)
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100325 unsetArgs = append(unsetArgs, "node", "remove", "role", "KubernetesWorker", nid)
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200326 if err := mctlRun(t, ctx, unsetArgs); err != nil {
327 return err
328 }
329 // Check that the role is unset.
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100330 if err := mctlFailIfFound(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200331 return err
332 }
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100333
334 return nil
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200335 })
336 })
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200337}