github.com/abayer/test-infra@v0.0.5/kubetest/dump_test.go (about) 1 /* 2 Copyright 2017 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package main 18 19 import ( 20 "context" 21 "fmt" 22 "io" 23 "io/ioutil" 24 "os" 25 "path/filepath" 26 "reflect" 27 "sort" 28 "strings" 29 "testing" 30 ) 31 32 func Test_logDumperNode_findFiles(t *testing.T) { 33 grid := []struct { 34 dir string 35 command string 36 stdout string 37 stderr string 38 expected []string 39 }{ 40 { 41 dir: "/var/log", 42 command: "sudo find /var/log -print0", 43 stdout: "/var/log/a\x00/var/log/b\x00", 44 expected: []string{ 45 "/var/log/a", 46 "/var/log/b", 47 }, 48 }, 49 { 50 dir: "/var/log", 51 command: "sudo find /var/log -print0", 52 stdout: "/var/log/a\x00", 53 expected: []string{ 54 "/var/log/a", 55 }, 56 }, 57 { 58 dir: "/var/log", 59 command: "sudo find /var/log -print0", 60 expected: []string{}, 61 }, 62 } 63 64 for _, g := range grid { 65 client := &mockSSHClient{} 66 n := logDumperNode{ 67 client: client, 68 } 69 70 client.commands = append(client.commands, &mockCommand{ 71 command: g.command, 72 stdout: []byte(g.stdout), 73 stderr: []byte(g.stderr), 74 }) 75 actual, err := n.findFiles(context.Background(), g.dir) 76 if err != nil { 77 t.Errorf("unexpected error from findFiles: %v", err) 78 continue 79 } 80 81 if !reflect.DeepEqual(actual, g.expected) { 82 t.Errorf("unexpected files. actual=%v, expected=%v", actual, g.expected) 83 continue 84 } 85 } 86 } 87 88 func Test_logDumperNode_listSystemdUnits(t *testing.T) { 89 grid := []struct { 90 command string 91 stdout string 92 stderr string 93 expected []string 94 }{ 95 { 96 command: "sudo systemctl list-units -t service --no-pager --no-legend --all", 97 stdout: "accounts-daemon.service loaded active running Accounts Service\n" + 98 "acpid.service loaded active running ACPI event daemon\n" + 99 "atd.service loaded active running Deferred execution scheduler\n" + 100 "avahi-daemon.service loaded active running Avahi mDNS/DNS-SD Stack\n" + 101 "bluetooth.service loaded active running Bluetooth service\n" + 102 "cameras.service loaded failed failed cameras\n" + 103 "colord.service loaded active running Manage, Install and Generate Color Profiles\n", 104 expected: []string{ 105 "accounts-daemon.service", 106 "acpid.service", 107 "atd.service", 108 "avahi-daemon.service", 109 "bluetooth.service", 110 "cameras.service", 111 "colord.service", 112 }, 113 }, 114 } 115 116 for _, g := range grid { 117 client := &mockSSHClient{} 118 n := logDumperNode{ 119 client: client, 120 } 121 122 client.commands = append(client.commands, &mockCommand{ 123 command: g.command, 124 stdout: []byte(g.stdout), 125 stderr: []byte(g.stderr), 126 }) 127 actual, err := n.listSystemdUnits(context.Background()) 128 if err != nil { 129 t.Errorf("unexpected error from listSystemdUnits: %v", err) 130 continue 131 } 132 133 if !reflect.DeepEqual(actual, g.expected) { 134 t.Errorf("unexpected systemdUnits. actual=%v, expected=%v", actual, g.expected) 135 continue 136 } 137 } 138 } 139 140 func Test_logDumperNode_shellToFile(t *testing.T) { 141 grid := []struct { 142 command string 143 stdout []byte 144 stderr []byte 145 }{ 146 { 147 command: "cat something", 148 stdout: []byte("hello"), 149 }, 150 } 151 152 for _, g := range grid { 153 client := &mockSSHClient{} 154 n := logDumperNode{ 155 client: client, 156 } 157 158 client.commands = append(client.commands, &mockCommand{ 159 command: g.command, 160 stdout: []byte(g.stdout), 161 stderr: []byte(g.stderr), 162 }) 163 164 tmpfile, err := ioutil.TempFile("", "") 165 if err != nil { 166 t.Errorf("error creating temp file: %v", err) 167 continue 168 } 169 170 defer func() { 171 if err := os.Remove(tmpfile.Name()); err != nil { 172 t.Errorf("error removing temp file: %v", err) 173 } 174 }() 175 176 err = n.shellToFile(context.Background(), "cat something", tmpfile.Name()) 177 if err != nil { 178 t.Errorf("unexpected error from shellToFile: %v", err) 179 continue 180 } 181 182 if err := tmpfile.Close(); err != nil { 183 t.Errorf("unexpected error closing file: %v", err) 184 continue 185 } 186 187 actual, err := ioutil.ReadFile(tmpfile.Name()) 188 if err != nil { 189 t.Errorf("unexpected error reading file: %v", err) 190 continue 191 } 192 193 if !reflect.DeepEqual(actual, g.stdout) { 194 t.Errorf("unexpected systemdUnits. actual=%q, expected=%q", string(actual), string(g.stdout)) 195 continue 196 } 197 } 198 } 199 200 func Test_logDumperNode_dump(t *testing.T) { 201 tmpdir, err := ioutil.TempDir("", "") 202 if err != nil { 203 t.Errorf("error creating temp dir: %v", err) 204 return 205 } 206 207 defer func() { 208 if err := os.RemoveAll(tmpdir); err != nil { 209 t.Errorf("error removing temp dir: %v", err) 210 } 211 }() 212 213 host1Client := &mockSSHClient{} 214 host1Client.commands = append(host1Client.commands, 215 &mockCommand{ 216 command: "sudo journalctl --output=short-precise -k", 217 }, 218 &mockCommand{ 219 command: "sudo systemctl list-units -t service --no-pager --no-legend --all", 220 stdout: []byte( 221 "kubelet.service loaded active running kubelet daemon\n" + 222 "atd.service loaded active running Deferred execution scheduler\n" + 223 "avahi-daemon.service loaded active running Avahi mDNS/DNS-SD Stack\n" + 224 "bluetooth.service loaded active running Bluetooth service\n" + 225 "cameras.service loaded failed failed cameras\n" + 226 "colord.service loaded active running Manage, Install and Generate Color Profiles\n", 227 ), 228 }, 229 &mockCommand{ 230 command: "sudo find /var/log -print0", 231 stdout: []byte(strings.Join([]string{ 232 "/var/log", 233 "/var/log/kube-controller-manager.log", 234 "/var/log/kube-controller-manager.log.1", 235 "/var/log/kube-controller-manager.log.2.gz", 236 "/var/log/other.log", 237 }, "\x00")), 238 }, 239 &mockCommand{ 240 command: "sudo journalctl --output=cat -u kubelet.service", 241 }, 242 &mockCommand{ 243 command: "sudo cat /var/log/kube-controller-manager.log", 244 }, 245 &mockCommand{ 246 command: "sudo cat /var/log/kube-controller-manager.log.1", 247 }, 248 &mockCommand{ 249 command: "sudo cat /var/log/kube-controller-manager.log.2.gz", 250 }, 251 ) 252 mockSSHClientFactory := &mockSSHClientFactory{ 253 clients: map[string]sshClient{ 254 "host1": host1Client, 255 }, 256 } 257 258 dumper, err := newLogDumper(mockSSHClientFactory, tmpdir) 259 if err != nil { 260 t.Errorf("error building logDumper: %v", err) 261 } 262 263 n, err := dumper.connectToNode(context.Background(), "nodename1", "host1") 264 if err != nil { 265 t.Errorf("error from connectToNode: %v", err) 266 } 267 268 errors := n.dump(context.Background()) 269 if len(errors) != 0 { 270 t.Errorf("unexpected errors from dump: %v", errors) 271 return 272 } 273 274 actual := []string{} 275 err = filepath.Walk(tmpdir, func(path string, info os.FileInfo, err error) error { 276 if err != nil { 277 return err 278 } 279 p := strings.TrimPrefix(strings.TrimPrefix(path, tmpdir), "/") 280 if p == "" { 281 return nil 282 } 283 if info.IsDir() { 284 p += "/" 285 } 286 actual = append(actual, p) 287 return nil 288 }) 289 if err != nil { 290 t.Errorf("unexpected error walking output tree: %v", err) 291 return 292 } 293 294 expected := []string{ 295 "nodename1/", 296 "nodename1/kern.log", 297 "nodename1/kubelet.log", 298 "nodename1/kube-controller-manager.log", 299 "nodename1/kube-controller-manager.log.1", 300 "nodename1/kube-controller-manager.log.2.gz", 301 } 302 303 sort.Strings(actual) 304 sort.Strings(expected) 305 306 if !reflect.DeepEqual(actual, expected) { 307 t.Errorf("unexpected files found in dump: actual=%v, expected=%v", actual, expected) 308 return 309 } 310 } 311 312 // mockCommand is an expected command and canned response 313 type mockCommand struct { 314 command string 315 stdout []byte 316 stderr []byte 317 err error 318 } 319 320 // mockSSHClient is a mock implementation of sshClient 321 type mockSSHClient struct { 322 // commands holds the canned commands and responses we expect 323 commands []*mockCommand 324 // closed is set if the mockSSHClient is closed 325 closed bool 326 } 327 328 var _ sshClient = &mockSSHClient{} 329 330 // mockSSHClientFactory is a mock implementation of sshClientFactory 331 type mockSSHClientFactory struct { 332 clients map[string]sshClient 333 } 334 335 var _ sshClientFactory = &mockSSHClientFactory{} 336 337 func (f *mockSSHClientFactory) Dial(ctx context.Context, host string) (sshClient, error) { 338 client := f.clients[host] 339 if client == nil { 340 return nil, fmt.Errorf("host %q not registered in mockSSHClientFactory", host) 341 } 342 return client, nil 343 } 344 345 // Close implements sshClient::Close. It records that the client was closed; future calls will fail 346 func (m *mockSSHClient) Close() error { 347 if m.closed { 348 return fmt.Errorf("mockSSHClient::Close called on Closed mockSSHClient") 349 } 350 m.closed = true 351 return nil 352 } 353 354 // ExecPiped implements sshClient::ExecPiped. It scans the configured commands, and returns the result if one is found. 355 // If no command is found, it returns an error. 356 func (m *mockSSHClient) ExecPiped(ctx context.Context, command string, stdout io.Writer, stderr io.Writer) error { 357 if m.closed { 358 return fmt.Errorf("mockSSHClient::ExecPiped called on Closed mockSSHClient") 359 } 360 for i := range m.commands { 361 c := m.commands[i] 362 if c == nil { 363 continue 364 } 365 if c.command == command { 366 if _, err := stdout.Write(c.stdout); err != nil { 367 return fmt.Errorf("error writing to stdout: %v", err) 368 } 369 if _, err := stderr.Write(c.stderr); err != nil { 370 return fmt.Errorf("error writing to stderr: %v", err) 371 } 372 m.commands[i] = nil 373 return c.err 374 } 375 } 376 377 return fmt.Errorf("unexpected command: %s", command) 378 } 379 380 func TestFindInstancesNotDumped(t *testing.T) { 381 n1 := &node{ 382 Status: nodeStatus{ 383 Addresses: []nodeAddress{{Address: "10.0.0.1"}}, 384 }, 385 } 386 387 n2 := &node{ 388 Status: nodeStatus{ 389 Addresses: []nodeAddress{{Address: "10.0.0.2"}}, 390 }, 391 } 392 n3 := &node{ 393 Status: nodeStatus{ 394 Addresses: []nodeAddress{ 395 {Address: "10.0.0.3"}, 396 {Address: "10.0.3.3"}, 397 }, 398 }, 399 } 400 401 grid := []struct { 402 ips []string 403 dumped []*node 404 expected []string 405 }{ 406 { 407 ips: nil, 408 dumped: nil, 409 expected: nil, 410 }, 411 { 412 ips: []string{"10.0.0.1"}, 413 dumped: nil, 414 expected: []string{"10.0.0.1"}, 415 }, 416 { 417 ips: []string{"10.0.0.1"}, 418 dumped: []*node{n1}, 419 expected: nil, 420 }, 421 { 422 ips: []string{"10.0.0.1", "10.0.0.2"}, 423 dumped: []*node{n1}, 424 expected: []string{"10.0.0.2"}, 425 }, 426 { 427 ips: []string{"10.0.0.1", "10.0.0.2", "10.0.3.3"}, 428 dumped: []*node{n1, n2, n3}, 429 expected: nil, 430 }, 431 { 432 ips: []string{"10.0.0.1", "10.0.0.2", "10.0.3.3"}, 433 dumped: []*node{n1, n2}, 434 expected: []string{"10.0.3.3"}, 435 }, 436 } 437 438 for _, g := range grid { 439 actual := findInstancesNotDumped(g.ips, g.dumped) 440 441 if !reflect.DeepEqual(actual, g.expected) { 442 t.Errorf("unexpected result from findInstancesNotDumped. actual=%v, expected=%v", actual, g.expected) 443 } 444 } 445 }