blob: 7b3191878db0e031df0f0eb28ab698b742270c2e [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
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020018 "source.monogon.dev/metropolis/pkg/cmd"
19 "source.monogon.dev/metropolis/test/launch/cluster"
20 "source.monogon.dev/metropolis/test/util"
Serge Bazanski0ccc85b2023-11-20 12:59:20 +010021 "source.monogon.dev/version"
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020022)
23
Mateusz Zalegab838e052022-08-12 18:08:10 +020024// resolveMetroctl resolves metroctl filesystem path. It will return a correct
25// path, or terminate test execution.
26func resolveMetroctl() string {
Tim Windelschmidt2a1d1b22024-02-06 07:07:42 +010027 path, err := runfiles.Rlocation("_main/metropolis/cli/metroctl/metroctl_/metroctl")
Mateusz Zalegab838e052022-08-12 18:08:10 +020028 if err != nil {
29 log.Fatalf("Couldn't resolve metroctl binary: %v", err)
30 }
31 return path
32}
33
34// mctlRun starts metroctl, and waits till it exits. It returns nil, if the run
35// was successful.
36func mctlRun(t *testing.T, ctx context.Context, args []string) error {
37 t.Helper()
38
39 path := resolveMetroctl()
40 log.Printf("$ metroctl %s", strings.Join(args, " "))
41 logf := func(line string) {
42 log.Printf("metroctl: %s", line)
43 }
44 _, err := cmd.RunCommand(ctx, path, args, cmd.WaitUntilCompletion(logf))
45 return err
46}
47
Mateusz Zalegadb75e212022-08-04 17:31:34 +020048// mctlExpectOutput returns true in the event the expected string is found in
49// metroctl output, and false otherwise.
50func mctlExpectOutput(t *testing.T, ctx context.Context, args []string, expect string) (bool, error) {
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020051 t.Helper()
52
Mateusz Zalegab838e052022-08-12 18:08:10 +020053 path := resolveMetroctl()
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020054 log.Printf("$ metroctl %s", strings.Join(args, " "))
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020055 // Terminate metroctl as soon as the expected output is found.
Mateusz Zalegab838e052022-08-12 18:08:10 +020056 logf := func(line string) {
57 log.Printf("metroctl: %s", line)
58 }
59 found, err := cmd.RunCommand(ctx, path, args, cmd.TerminateIfFound(expect, logf))
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020060 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +020061 return false, fmt.Errorf("while running metroctl: %w", err)
Mateusz Zalegadb75e212022-08-04 17:31:34 +020062 }
63 return found, nil
64}
65
66// mctlFailIfMissing will return a non-nil error value either if the expected
67// output string s is missing in metroctl output, or in case metroctl can't be
68// launched.
69func mctlFailIfMissing(t *testing.T, ctx context.Context, args []string, s string) error {
70 found, err := mctlExpectOutput(t, ctx, args, s)
71 if err != nil {
72 return err
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020073 }
74 if !found {
Mateusz Zalegadb75e212022-08-04 17:31:34 +020075 return fmt.Errorf("expected output is missing: \"%s\"", s)
76 }
77 return nil
78}
79
80// mctlFailIfFound will return a non-nil error value either if the expected
81// output string s is found in metroctl output, or in case metroctl can't be
82// launched.
83func mctlFailIfFound(t *testing.T, ctx context.Context, args []string, s string) error {
84 found, err := mctlExpectOutput(t, ctx, args, s)
85 if err != nil {
86 return err
87 }
88 if found {
89 return fmt.Errorf("unexpected output was found: \"%s\"", s)
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020090 }
91 return nil
92}
93
94func TestMetroctl(t *testing.T) {
95 ctx, ctxC := context.WithCancel(context.Background())
96 defer ctxC()
97
98 co := cluster.ClusterOptions{
99 NumNodes: 2,
100 }
101 cl, err := cluster.LaunchCluster(context.Background(), co)
102 if err != nil {
103 t.Fatalf("LaunchCluster failed: %v", err)
104 }
105 defer func() {
106 err := cl.Close()
107 if err != nil {
108 t.Fatalf("cluster Close failed: %v", err)
109 }
110 }()
111
112 socksRemote := fmt.Sprintf("localhost:%d", cl.Ports[cluster.SOCKSPort])
113 var clusterEndpoints []string
Lorenz Brun3a3c5172023-06-01 19:54:17 +0200114 // Use node starting order for endpoints
115 for _, ep := range cl.NodeIDs {
116 clusterEndpoints = append(clusterEndpoints, cl.Nodes[ep].ManagementAddress)
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200117 }
118
119 ownerPem := pem.EncodeToMemory(&pem.Block{
120 Type: "METROPOLIS INITIAL OWNER PRIVATE KEY",
121 Bytes: cluster.InsecurePrivateKey,
122 })
123 if err := os.WriteFile("owner-key.pem", ownerPem, 0644); err != nil {
124 log.Fatal("Couldn't write owner-key.pem")
125 }
126
127 commonOpts := []string{
128 "--proxy=" + socksRemote,
129 "--config=.",
Serge Bazanski7eeef0f2024-02-05 14:40:15 +0100130 "--insecure-accept-and-persist-first-encountered-ca",
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200131 }
132
133 var endpointOpts []string
134 for _, ep := range clusterEndpoints {
135 endpointOpts = append(endpointOpts, "--endpoints="+ep)
136 }
137
138 log.Printf("metroctl: Cluster's running, starting tests...")
139 st := t.Run("Init", func(t *testing.T) {
140 util.TestEventual(t, "metroctl takeownership", ctx, 30*time.Second, func(ctx context.Context) error {
141 // takeownership needs just a single endpoint pointing at the initial node.
142 var args []string
143 args = append(args, commonOpts...)
144 args = append(args, endpointOpts[0])
145 args = append(args, "takeownership")
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200146 return mctlFailIfMissing(t, ctx, args, "Successfully retrieved owner credentials")
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200147 })
148 })
149 if !st {
150 t.Fatalf("metroctl: Couldn't get cluster ownership.")
151 }
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200152 t.Run("list", func(t *testing.T) {
153 util.TestEventual(t, "metroctl list", ctx, 10*time.Second, func(ctx context.Context) error {
154 var args []string
155 args = append(args, commonOpts...)
156 args = append(args, endpointOpts...)
157 args = append(args, "node", "list")
158 // Expect both node IDs to show up in the results.
159 if err := mctlFailIfMissing(t, ctx, args, cl.NodeIDs[0]); err != nil {
160 return err
161 }
162 return mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1])
163 })
164 })
165 t.Run("list [nodeID]", func(t *testing.T) {
166 util.TestEventual(t, "metroctl list [nodeID]", ctx, 10*time.Second, func(ctx context.Context) error {
167 var args []string
168 args = append(args, commonOpts...)
169 args = append(args, endpointOpts...)
170 args = append(args, "node", "list", cl.NodeIDs[1])
171 // Expect just the supplied node IDs to show up in the results.
172 if err := mctlFailIfFound(t, ctx, args, cl.NodeIDs[0]); err != nil {
173 return err
174 }
175 return mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1])
176 })
177 })
178 t.Run("list --output", func(t *testing.T) {
179 util.TestEventual(t, "metroctl list --output", ctx, 10*time.Second, func(ctx context.Context) error {
180 var args []string
181 args = append(args, commonOpts...)
182 args = append(args, endpointOpts...)
183 args = append(args, "node", "list", "--output", "list.txt")
184 // In this case metroctl should write its output to a file rather than stdout.
185 if err := mctlFailIfFound(t, ctx, args, cl.NodeIDs[0]); err != nil {
186 return err
187 }
188 od, err := os.ReadFile("list.txt")
189 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +0200190 return fmt.Errorf("while reading metroctl output file: %w", err)
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200191 }
192 if !strings.Contains(string(od), cl.NodeIDs[0]) {
193 return fmt.Errorf("expected node ID hasn't been found in metroctl output")
194 }
195 return nil
196 })
197 })
198 t.Run("list --filter", func(t *testing.T) {
199 util.TestEventual(t, "metroctl list --filter", ctx, 10*time.Second, func(ctx context.Context) error {
200 nid := cl.NodeIDs[1]
201 naddr := cl.Nodes[nid].ManagementAddress
202
203 var args []string
204 args = append(args, commonOpts...)
205 args = append(args, endpointOpts...)
206 // Filter list results based on nodes' external addresses.
207 args = append(args, "node", "list", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
208 // Expect the second node's ID to show up in the results.
209 if err := mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1]); err != nil {
210 return err
211 }
212 // The first node should've been filtered away.
213 return mctlFailIfFound(t, ctx, args, cl.NodeIDs[0])
214 })
215 })
Mateusz Zalegab838e052022-08-12 18:08:10 +0200216 t.Run("describe --filter", func(t *testing.T) {
217 util.TestEventual(t, "metroctl list --filter", ctx, 10*time.Second, func(ctx context.Context) error {
218 nid := cl.NodeIDs[0]
219 naddr := cl.Nodes[nid].ManagementAddress
220
221 var args []string
222 args = append(args, commonOpts...)
223 args = append(args, endpointOpts...)
224
225 // Filter out the first node. Afterwards, only one node should be left.
226 args = append(args, "node", "describe", "--output", "describe.txt", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
227 if err := mctlRun(t, ctx, args); err != nil {
228 return err
229 }
230
231 // Try matching metroctl output against the advertised format.
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100232 f, err := os.Open("describe.txt")
Mateusz Zalegab838e052022-08-12 18:08:10 +0200233 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +0200234 return fmt.Errorf("while opening metroctl output: %w", err)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200235 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100236 scanner := bufio.NewScanner(f)
237 if !scanner.Scan() {
238 return fmt.Errorf("expected header line")
239 }
240 if !scanner.Scan() {
241 return fmt.Errorf("expected result line")
242 }
243 line := scanner.Text()
244 t.Logf("Line: %q", line)
245
Serge Bazanski0ccc85b2023-11-20 12:59:20 +0100246 var onid, ostate, onaddr, onstatus, onroles, ontpm, onver string
Mateusz Zalegab838e052022-08-12 18:08:10 +0200247 var ontimeout int
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100248
Serge Bazanski0ccc85b2023-11-20 12:59:20 +0100249 _, 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 +0200250 if err != nil {
Tim Windelschmidt58786032024-05-21 13:47:41 +0200251 return fmt.Errorf("while parsing metroctl output: %w", err)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200252 }
253 if onid != nid {
254 return fmt.Errorf("node id mismatch")
255 }
Serge Bazanskicfbbbdb2023-03-22 17:48:08 +0100256 if ostate != "UP" {
Mateusz Zalegab838e052022-08-12 18:08:10 +0200257 return fmt.Errorf("node state mismatch")
258 }
259 if onaddr != naddr {
260 return fmt.Errorf("node address mismatch")
261 }
262 if onstatus != "HEALTHY" {
263 return fmt.Errorf("node status mismatch")
264 }
Serge Bazanski15f7f632023-03-14 17:17:20 +0100265 if want, got := "ConsensusMember,KubernetesController", onroles; want != got {
266 return fmt.Errorf("node role mismatch: wanted %q, got %q", want, got)
Mateusz Zalegab838e052022-08-12 18:08:10 +0200267 }
Serge Bazanskie4a4ce12023-03-22 18:29:54 +0100268 if want, got := "yes", ontpm; want != got {
269 return fmt.Errorf("node tpm mismatch: wanted %q, got %q", want, got)
270 }
Serge Bazanski0ccc85b2023-11-20 12:59:20 +0100271 if want, got := version.Semver(mversion.Version), onver; want != got {
272 return fmt.Errorf("node version mismatch: wanted %q, got %q", want, got)
273 }
Mateusz Zalegab838e052022-08-12 18:08:10 +0200274 if ontimeout < 0 || ontimeout > 30 {
275 return fmt.Errorf("node timeout mismatch")
276 }
277 return nil
278 })
279 })
Serge Bazanskie012b722023-03-29 17:49:04 +0200280 t.Run("logs [nodeID]", func(t *testing.T) {
281 util.TestEventual(t, "metroctl logs [nodeID]", ctx, 10*time.Second, func(ctx context.Context) error {
282 var args []string
283 args = append(args, commonOpts...)
284 args = append(args, endpointOpts...)
285 args = append(args, "node", "logs", cl.NodeIDs[1])
286
287 if err := mctlFailIfMissing(t, ctx, args, "Cluster enrolment done."); err != nil {
288 return err
289 }
290
291 return nil
292 })
293 })
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200294 t.Run("set/unset role", func(t *testing.T) {
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100295 util.TestEventual(t, "metroctl set/unset role KubernetesWorker", ctx, 10*time.Second, func(ctx context.Context) error {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200296 nid := cl.NodeIDs[1]
297 naddr := cl.Nodes[nid].ManagementAddress
298
299 // In this test we'll unset a node role, make sure that it's been in fact
300 // unset, then set it again, and check again. This exercises commands of
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100301 // the form "metroctl set/unset role KubernetesWorker [NodeID, ...]".
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200302
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100303 // Check that KubernetesWorker role is absent initially.
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200304 var describeArgs []string
305 describeArgs = append(describeArgs, commonOpts...)
306 describeArgs = append(describeArgs, endpointOpts...)
307 describeArgs = append(describeArgs, "node", "describe", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100308 if err := mctlFailIfFound(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200309 return err
310 }
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100311 // Add the role.
312 var setArgs []string
313 setArgs = append(setArgs, commonOpts...)
314 setArgs = append(setArgs, endpointOpts...)
315 setArgs = append(setArgs, "node", "add", "role", "KubernetesWorker", nid)
316 if err := mctlRun(t, ctx, setArgs); err != nil {
317 return err
318 }
319 // Check that the role is set.
320 if err := mctlFailIfMissing(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
321 return err
322 }
323
324 // Remove the role back to the initial value.
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200325 var unsetArgs []string
326 unsetArgs = append(unsetArgs, commonOpts...)
327 unsetArgs = append(unsetArgs, endpointOpts...)
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100328 unsetArgs = append(unsetArgs, "node", "remove", "role", "KubernetesWorker", nid)
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200329 if err := mctlRun(t, ctx, unsetArgs); err != nil {
330 return err
331 }
332 // Check that the role is unset.
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100333 if err := mctlFailIfFound(t, ctx, describeArgs, "KubernetesWorker"); err != nil {
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200334 return err
335 }
Serge Bazanski2cfafc92023-03-21 16:42:47 +0100336
337 return nil
Mateusz Zalegae15fee12022-08-12 18:48:40 +0200338 })
339 })
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200340}