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  }