blob: 0d9b2e11c74c97f80ab2551c4b7f73f50d5b6b80 [file] [log] [blame]
Mateusz Zalegafed8fe52022-07-14 16:19:35 +02001package test
2
3import (
4 "context"
5 "encoding/pem"
6 "fmt"
7 "log"
8 "os"
9 "strings"
10 "testing"
11 "time"
12
13 "source.monogon.dev/metropolis/cli/pkg/datafile"
14 "source.monogon.dev/metropolis/pkg/cmd"
15 "source.monogon.dev/metropolis/test/launch/cluster"
16 "source.monogon.dev/metropolis/test/util"
17)
18
Mateusz Zalegab838e052022-08-12 18:08:10 +020019// resolveMetroctl resolves metroctl filesystem path. It will return a correct
20// path, or terminate test execution.
21func resolveMetroctl() string {
22 path, err := datafile.ResolveRunfile("metropolis/cli/metroctl/metroctl_/metroctl")
23 if err != nil {
24 log.Fatalf("Couldn't resolve metroctl binary: %v", err)
25 }
26 return path
27}
28
29// mctlRun starts metroctl, and waits till it exits. It returns nil, if the run
30// was successful.
31func mctlRun(t *testing.T, ctx context.Context, args []string) error {
32 t.Helper()
33
34 path := resolveMetroctl()
35 log.Printf("$ metroctl %s", strings.Join(args, " "))
36 logf := func(line string) {
37 log.Printf("metroctl: %s", line)
38 }
39 _, err := cmd.RunCommand(ctx, path, args, cmd.WaitUntilCompletion(logf))
40 return err
41}
42
Mateusz Zalegadb75e212022-08-04 17:31:34 +020043// mctlExpectOutput returns true in the event the expected string is found in
44// metroctl output, and false otherwise.
45func mctlExpectOutput(t *testing.T, ctx context.Context, args []string, expect string) (bool, error) {
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020046 t.Helper()
47
Mateusz Zalegab838e052022-08-12 18:08:10 +020048 path := resolveMetroctl()
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020049 log.Printf("$ metroctl %s", strings.Join(args, " "))
Mateusz Zalega6cdc9762022-08-03 17:15:01 +020050 // Terminate metroctl as soon as the expected output is found.
Mateusz Zalegab838e052022-08-12 18:08:10 +020051 logf := func(line string) {
52 log.Printf("metroctl: %s", line)
53 }
54 found, err := cmd.RunCommand(ctx, path, args, cmd.TerminateIfFound(expect, logf))
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020055 if err != nil {
Mateusz Zalegadb75e212022-08-04 17:31:34 +020056 return false, fmt.Errorf("while running metroctl: %v", err)
57 }
58 return found, nil
59}
60
61// mctlFailIfMissing will return a non-nil error value either if the expected
62// output string s is missing in metroctl output, or in case metroctl can't be
63// launched.
64func mctlFailIfMissing(t *testing.T, ctx context.Context, args []string, s string) error {
65 found, err := mctlExpectOutput(t, ctx, args, s)
66 if err != nil {
67 return err
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020068 }
69 if !found {
Mateusz Zalegadb75e212022-08-04 17:31:34 +020070 return fmt.Errorf("expected output is missing: \"%s\"", s)
71 }
72 return nil
73}
74
75// mctlFailIfFound will return a non-nil error value either if the expected
76// output string s is found in metroctl output, or in case metroctl can't be
77// launched.
78func mctlFailIfFound(t *testing.T, ctx context.Context, args []string, s string) error {
79 found, err := mctlExpectOutput(t, ctx, args, s)
80 if err != nil {
81 return err
82 }
83 if found {
84 return fmt.Errorf("unexpected output was found: \"%s\"", s)
Mateusz Zalegafed8fe52022-07-14 16:19:35 +020085 }
86 return nil
87}
88
89func TestMetroctl(t *testing.T) {
90 ctx, ctxC := context.WithCancel(context.Background())
91 defer ctxC()
92
93 co := cluster.ClusterOptions{
94 NumNodes: 2,
95 }
96 cl, err := cluster.LaunchCluster(context.Background(), co)
97 if err != nil {
98 t.Fatalf("LaunchCluster failed: %v", err)
99 }
100 defer func() {
101 err := cl.Close()
102 if err != nil {
103 t.Fatalf("cluster Close failed: %v", err)
104 }
105 }()
106
107 socksRemote := fmt.Sprintf("localhost:%d", cl.Ports[cluster.SOCKSPort])
108 var clusterEndpoints []string
109 for _, ep := range cl.Nodes {
110 clusterEndpoints = append(clusterEndpoints, ep.ManagementAddress)
111 }
112
113 ownerPem := pem.EncodeToMemory(&pem.Block{
114 Type: "METROPOLIS INITIAL OWNER PRIVATE KEY",
115 Bytes: cluster.InsecurePrivateKey,
116 })
117 if err := os.WriteFile("owner-key.pem", ownerPem, 0644); err != nil {
118 log.Fatal("Couldn't write owner-key.pem")
119 }
120
121 commonOpts := []string{
122 "--proxy=" + socksRemote,
123 "--config=.",
124 }
125
126 var endpointOpts []string
127 for _, ep := range clusterEndpoints {
128 endpointOpts = append(endpointOpts, "--endpoints="+ep)
129 }
130
131 log.Printf("metroctl: Cluster's running, starting tests...")
132 st := t.Run("Init", func(t *testing.T) {
133 util.TestEventual(t, "metroctl takeownership", ctx, 30*time.Second, func(ctx context.Context) error {
134 // takeownership needs just a single endpoint pointing at the initial node.
135 var args []string
136 args = append(args, commonOpts...)
137 args = append(args, endpointOpts[0])
138 args = append(args, "takeownership")
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200139 return mctlFailIfMissing(t, ctx, args, "Successfully retrieved owner credentials")
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200140 })
141 })
142 if !st {
143 t.Fatalf("metroctl: Couldn't get cluster ownership.")
144 }
Mateusz Zalegadb75e212022-08-04 17:31:34 +0200145 t.Run("list", func(t *testing.T) {
146 util.TestEventual(t, "metroctl list", ctx, 10*time.Second, func(ctx context.Context) error {
147 var args []string
148 args = append(args, commonOpts...)
149 args = append(args, endpointOpts...)
150 args = append(args, "node", "list")
151 // Expect both node IDs to show up in the results.
152 if err := mctlFailIfMissing(t, ctx, args, cl.NodeIDs[0]); err != nil {
153 return err
154 }
155 return mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1])
156 })
157 })
158 t.Run("list [nodeID]", func(t *testing.T) {
159 util.TestEventual(t, "metroctl list [nodeID]", ctx, 10*time.Second, func(ctx context.Context) error {
160 var args []string
161 args = append(args, commonOpts...)
162 args = append(args, endpointOpts...)
163 args = append(args, "node", "list", cl.NodeIDs[1])
164 // Expect just the supplied node IDs to show up in the results.
165 if err := mctlFailIfFound(t, ctx, args, cl.NodeIDs[0]); err != nil {
166 return err
167 }
168 return mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1])
169 })
170 })
171 t.Run("list --output", func(t *testing.T) {
172 util.TestEventual(t, "metroctl list --output", ctx, 10*time.Second, func(ctx context.Context) error {
173 var args []string
174 args = append(args, commonOpts...)
175 args = append(args, endpointOpts...)
176 args = append(args, "node", "list", "--output", "list.txt")
177 // In this case metroctl should write its output to a file rather than stdout.
178 if err := mctlFailIfFound(t, ctx, args, cl.NodeIDs[0]); err != nil {
179 return err
180 }
181 od, err := os.ReadFile("list.txt")
182 if err != nil {
183 return fmt.Errorf("while reading metroctl output file: %v", err)
184 }
185 if !strings.Contains(string(od), cl.NodeIDs[0]) {
186 return fmt.Errorf("expected node ID hasn't been found in metroctl output")
187 }
188 return nil
189 })
190 })
191 t.Run("list --filter", func(t *testing.T) {
192 util.TestEventual(t, "metroctl list --filter", ctx, 10*time.Second, func(ctx context.Context) error {
193 nid := cl.NodeIDs[1]
194 naddr := cl.Nodes[nid].ManagementAddress
195
196 var args []string
197 args = append(args, commonOpts...)
198 args = append(args, endpointOpts...)
199 // Filter list results based on nodes' external addresses.
200 args = append(args, "node", "list", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
201 // Expect the second node's ID to show up in the results.
202 if err := mctlFailIfMissing(t, ctx, args, cl.NodeIDs[1]); err != nil {
203 return err
204 }
205 // The first node should've been filtered away.
206 return mctlFailIfFound(t, ctx, args, cl.NodeIDs[0])
207 })
208 })
Mateusz Zalegab838e052022-08-12 18:08:10 +0200209 t.Run("describe --filter", func(t *testing.T) {
210 util.TestEventual(t, "metroctl list --filter", ctx, 10*time.Second, func(ctx context.Context) error {
211 nid := cl.NodeIDs[0]
212 naddr := cl.Nodes[nid].ManagementAddress
213
214 var args []string
215 args = append(args, commonOpts...)
216 args = append(args, endpointOpts...)
217
218 // Filter out the first node. Afterwards, only one node should be left.
219 args = append(args, "node", "describe", "--output", "describe.txt", "--filter", fmt.Sprintf("node.status.external_address==\"%s\"", naddr))
220 if err := mctlRun(t, ctx, args); err != nil {
221 return err
222 }
223
224 // Try matching metroctl output against the advertised format.
225 ob, err := os.ReadFile("describe.txt")
226 if err != nil {
227 return fmt.Errorf("while reading metroctl output: %v", err)
228 }
229 var onid, ostate, onaddr, onstatus, onroles string
230 var ontimeout int
231 _, err = fmt.Sscanf(string(ob[:]), "%s\t%s\t%s\t%s\t%s\t%ds\n", &onid, &ostate, &onaddr, &onstatus, &onroles, &ontimeout)
232 if err != nil {
233 return fmt.Errorf("while parsing metroctl output: %v", err)
234 }
235 if onid != nid {
236 return fmt.Errorf("node id mismatch")
237 }
238 if ostate != "NODE_STATE_UP" {
239 return fmt.Errorf("node state mismatch")
240 }
241 if onaddr != naddr {
242 return fmt.Errorf("node address mismatch")
243 }
244 if onstatus != "HEALTHY" {
245 return fmt.Errorf("node status mismatch")
246 }
247 if onroles != "KubernetesWorker,ConsensusMember" {
248 return fmt.Errorf("node role mismatch")
249 }
250 if ontimeout < 0 || ontimeout > 30 {
251 return fmt.Errorf("node timeout mismatch")
252 }
253 return nil
254 })
255 })
Mateusz Zalegafed8fe52022-07-14 16:19:35 +0200256}