blob: b031271c2ba5443c666610d65e9c3be8445a2aca [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=.",
Serge Bazanski7eeef0f2024-02-05 14:40:15 +0100128 "--insecure-accept-and-persist-first-encountered-ca",
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200129 }
130
131 var endpointOpts []string
132 for _, ep := range clusterEndpoints {
133 endpointOpts = append(endpointOpts, "--endpoints="+ep)
134 }
135
136 log.Printf("metroctl: Cluster's running, starting tests...")
137 st := t.Run("Init", func(t *testing.T) {
138 util.TestEventual(t, "metroctl takeownership", ctx, 30*time.Second, func(ctx context.Context) error {
139 // takeownership needs just a single endpoint pointing at the initial node.
140 var args []string
141 args = append(args, commonOpts...)
142 args = append(args, endpointOpts[0])
143 args = append(args, "takeownership")
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200144 return mctlFailIfMissing(t, ctx, args, "Successfully retrieved owner credentials")
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200145 })
146 })
147 if !st {
148 t.Fatalf("metroctl: Couldn't get cluster ownership.")
149 }
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200150 t.Run("list", func(t *testing.T) {
151 util.TestEventual(t, "metroctl list", ctx, 10*time.Second, func(ctx context.Context) error {
152 var args []string
153 args = append(args, commonOpts...)
154 args = append(args, endpointOpts...)
155 args = append(args, "node", "list")
156 // Expect both node IDs to show up in the results.
157 if err := mctlFailIfMissing(t, ctx, args, cl.NodeIDs[0]); err != nil {
158 return err
159 }
160 return mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1])
161 })
162 })
163 t.Run("list [nodeID]", func(t *testing.T) {
164 util.TestEventual(t, "metroctl list [nodeID]", 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", cl.NodeIDs[1])
169 // Expect just the supplied node IDs to show up in the results.
170 if err := mctlFailIfFound(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 --output", func(t *testing.T) {
177 util.TestEventual(t, "metroctl list --output", 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", "--output", "list.txt")
182 // In this case metroctl should write its output to a file rather than stdout.
183 if err := mctlFailIfFound(t, ctx, args, cl.NodeIDs[0]); err != nil {
184 return err
185 }
186 od, err := os.ReadFile("list.txt")
187 if err != nil {
188 return fmt.Errorf("while reading metroctl output file: %v", err)
189 }
190 if !strings.Contains(string(od), cl.NodeIDs[0]) {
191 return fmt.Errorf("expected node ID hasn't been found in metroctl output")
192 }
193 return nil
194 })
195 })
196 t.Run("list --filter", func(t *testing.T) {
197 util.TestEventual(t, "metroctl list --filter", ctx, 10*time.Second, func(ctx context.Context) error {
198 nid := cl.NodeIDs[1]
199 naddr := cl.Nodes[nid].ManagementAddress
200
201 var args []string
202 args = append(args, commonOpts...)
203 args = append(args, endpointOpts...)
204 // Filter list results based on nodes' external addresses.
205 args = append(args, "node", "list", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
206 // Expect the second node's ID to show up in the results.
207 if err := mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1]); err != nil {
208 return err
209 }
210 // The first node should've been filtered away.
211 return mctlFailIfFound(t, ctx, args, cl.NodeIDs[0])
212 })
213 })
Mateusz Zalegab838e052022-08-12 18:08:10 +0200214 t.Run("describe --filter", func(t *testing.T) {
215 util.TestEventual(t, "metroctl list --filter", ctx, 10*time.Second, func(ctx context.Context) error {
216 nid := cl.NodeIDs[0]
217 naddr := cl.Nodes[nid].ManagementAddress
218
219 var args []string
220 args = append(args, commonOpts...)
221 args = append(args, endpointOpts...)
222
223 // Filter out the first node. Afterwards, only one node should be left.
224 args = append(args, "node", "describe", "--output", "describe.txt", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
225 if err := mctlRun(t, ctx, args); err != nil {
226 return err
227 }
228
229 // Try matching metroctl output against the advertised format.
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100230 f, err := os.Open("describe.txt")
Mateusz Zalegab838e052022-08-12 18:08:10 +0200231 if err != nil {
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100232 return fmt.Errorf("while opening metroctl output: %v", err)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200233 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100234 scanner := bufio.NewScanner(f)
235 if !scanner.Scan() {
236 return fmt.Errorf("expected header line")
237 }
238 if !scanner.Scan() {
239 return fmt.Errorf("expected result line")
240 }
241 line := scanner.Text()
242 t.Logf("Line: %q", line)
243
Serge Bazanski0ccc85b2023-11-20 12:59:20 +0100244 var onid, ostate, onaddr, onstatus, onroles, ontpm, onver string
Mateusz Zalegab838e052022-08-12 18:08:10 +0200245 var ontimeout int
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100246
Serge Bazanski0ccc85b2023-11-20 12:59:20 +0100247 _, 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 +0200248 if err != nil {
249 return fmt.Errorf("while parsing metroctl output: %v", err)
250 }
251 if onid != nid {
252 return fmt.Errorf("node id mismatch")
253 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100254 if ostate != "UP" {
Mateusz Zalegab838e052022-08-12 18:08:10 +0200255 return fmt.Errorf("node state mismatch")
256 }
257 if onaddr != naddr {
258 return fmt.Errorf("node address mismatch")
259 }
260 if onstatus != "HEALTHY" {
261 return fmt.Errorf("node status mismatch")
262 }
Serge Bazanski15f7f632023-03-14 17:17:20 +0100263 if want, got := "ConsensusMember,KubernetesController", onroles; want != got {
264 return fmt.Errorf("node role mismatch: wanted %q, got %q", want, got)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200265 }
Serge Bazanskie4a4ce12023-03-22 18:29:54 +0100266 if want, got := "yes", ontpm; want != got {
267 return fmt.Errorf("node tpm mismatch: wanted %q, got %q", want, got)
268 }
Serge Bazanski0ccc85b2023-11-20 12:59:20 +0100269 if want, got := version.Semver(mversion.Version), onver; want != got {
270 return fmt.Errorf("node version mismatch: wanted %q, got %q", want, got)
271 }
Mateusz Zalegab838e052022-08-12 18:08:10 +0200272 if ontimeout < 0 || ontimeout > 30 {
273 return fmt.Errorf("node timeout mismatch")
274 }
275 return nil
276 })
277 })
Serge Bazanskie012b722023-03-29 17:49:04 +0200278 t.Run("logs [nodeID]", func(t *testing.T) {
279 util.TestEventual(t, "metroctl logs [nodeID]", ctx, 10*time.Second, func(ctx context.Context) error {
280 var args []string
281 args = append(args, commonOpts...)
282 args = append(args, endpointOpts...)
283 args = append(args, "node", "logs", cl.NodeIDs[1])
284
285 if err := mctlFailIfMissing(t, ctx, args, "Cluster enrolment done."); err != nil {
286 return err
287 }
288
289 return nil
290 })
291 })
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200292 t.Run("set/unset role", func(t *testing.T) {
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100293 util.TestEventual(t, "metroctl set/unset role KubernetesWorker", ctx, 10*time.Second, func(ctx context.Context) error {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200294 nid := cl.NodeIDs[1]
295 naddr := cl.Nodes[nid].ManagementAddress
296
297 // In this test we'll unset a node role, make sure that it's been in fact
298 // unset, then set it again, and check again. This exercises commands of
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100299 // the form "metroctl set/unset role KubernetesWorker [NodeID, ...]".
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200300
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100301 // Check that KubernetesWorker role is absent initially.
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200302 var describeArgs []string
303 describeArgs = append(describeArgs, commonOpts...)
304 describeArgs = append(describeArgs, endpointOpts...)
305 describeArgs = append(describeArgs, "node", "describe", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100306 if err := mctlFailIfFound(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200307 return err
308 }
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100309 // Add the role.
310 var setArgs []string
311 setArgs = append(setArgs, commonOpts...)
312 setArgs = append(setArgs, endpointOpts...)
313 setArgs = append(setArgs, "node", "add", "role", "KubernetesWorker", nid)
314 if err := mctlRun(t, ctx, setArgs); err != nil {
315 return err
316 }
317 // Check that the role is set.
318 if err := mctlFailIfMissing(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
319 return err
320 }
321
322 // Remove the role back to the initial value.
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200323 var unsetArgs []string
324 unsetArgs = append(unsetArgs, commonOpts...)
325 unsetArgs = append(unsetArgs, endpointOpts...)
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100326 unsetArgs = append(unsetArgs, "node", "remove", "role", "KubernetesWorker", nid)
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200327 if err := mctlRun(t, ctx, unsetArgs); err != nil {
328 return err
329 }
330 // Check that the role is unset.
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100331 if err := mctlFailIfFound(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200332 return err
333 }
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100334
335 return nil
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200336 })
337 })
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200338}