github.com/telepresenceio/telepresence/v2@v2.20.0-pro.6.0.20240517030216-236ea954e789/pkg/client/cli/cmd/gather_logs_test.go (about)

     1  package cmd
     2  
     3  import (
     4  	"archive/zip"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"regexp"
     9  	"testing"
    10  	"time"
    11  
    12  	"github.com/spf13/cobra"
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  
    16  	"github.com/datawire/dlib/dlog"
    17  	"github.com/telepresenceio/telepresence/v2/pkg/client/cli/connect"
    18  	"github.com/telepresenceio/telepresence/v2/pkg/filelocation"
    19  )
    20  
    21  func Test_gatherLogsZipFiles(t *testing.T) {
    22  	type testcase struct {
    23  		name string
    24  		// We use these two slices so it's easier to write tests knowing which
    25  		// files are expected to exist and which aren't. These slices are combined
    26  		// prior to calling zipFiles in the tests.
    27  		realFileNames []string
    28  		fakeFileNames []string
    29  		fileDir       string
    30  	}
    31  	testCases := []testcase{
    32  		{
    33  			name:          "successfulZipAllFiles",
    34  			realFileNames: []string{"file1.log", "file2.log", "diff_name.log"},
    35  			fakeFileNames: []string{},
    36  			fileDir:       "testdata/zipDir",
    37  		},
    38  		{
    39  			name:          "successfulZipSomeFiles",
    40  			realFileNames: []string{"file1.log", "file2.log"},
    41  			fakeFileNames: []string{},
    42  			fileDir:       "testdata/zipDir",
    43  		},
    44  		{
    45  			name:          "successfulZipNoFiles",
    46  			realFileNames: []string{},
    47  			fakeFileNames: []string{},
    48  			fileDir:       "testdata/zipDir",
    49  		},
    50  		{
    51  			name:          "zipOneIncorrectFile",
    52  			realFileNames: []string{"file1.log", "file2.log", "diff_name.log"},
    53  			fakeFileNames: []string{"notreal.log"},
    54  			fileDir:       "testdata/zipDir",
    55  		},
    56  		{
    57  			name:          "zipIncorrectDir",
    58  			realFileNames: []string{},
    59  			fakeFileNames: []string{"file1.log", "file2.log", "diff_name.log"},
    60  			fileDir:       "testdata/fakeZipDir",
    61  		},
    62  	}
    63  
    64  	for _, tc := range testCases {
    65  		tcName := tc.name
    66  		tc := tc
    67  		t.Run(tcName, func(t *testing.T) {
    68  			var fileNames []string
    69  			fileNames = append(fileNames, tc.realFileNames...)
    70  			fileNames = append(fileNames, tc.fakeFileNames...)
    71  			if tc.fileDir != "" {
    72  				for i := range fileNames {
    73  					fileNames[i] = fmt.Sprintf("%s/%s", tc.fileDir, fileNames[i])
    74  				}
    75  			}
    76  			outputDir := t.TempDir()
    77  			err := zipFiles(fileNames, fmt.Sprintf("%s/logs.zip", outputDir))
    78  			// If we put in fakeFileNames, then we verify we get the errors we expect
    79  			if len(tc.fakeFileNames) > 0 {
    80  				for _, name := range tc.fakeFileNames {
    81  					assert.Contains(t, err.Error(), fmt.Sprintf("failed adding %s/%s to zip file", tc.fileDir, name))
    82  				}
    83  			} else {
    84  				require.NoError(t, err)
    85  			}
    86  
    87  			// Ensure the files in the zip match the files that wer zipped
    88  			zipReader, err := zip.OpenReader(fmt.Sprintf("%s/logs.zip", outputDir))
    89  			require.NoError(t, err)
    90  			defer zipReader.Close()
    91  
    92  			for _, f := range zipReader.File {
    93  				// Ensure the file was actually supposed to be in the zip
    94  				assert.Contains(t, tc.realFileNames, f.Name)
    95  
    96  				filesEqual, err := checkZipEqual(f, "testdata/zipDir")
    97  				require.NoError(t, err)
    98  				assert.True(t, filesEqual)
    99  			}
   100  
   101  			// Ensure that only the "real files" were added to the zip file
   102  			assert.Equal(t, len(tc.realFileNames), len(zipReader.File))
   103  		})
   104  	}
   105  }
   106  
   107  func Test_gatherLogsCopyFiles(t *testing.T) {
   108  	type testcase struct {
   109  		name        string
   110  		srcFileName string
   111  		fileDir     string
   112  		outputDir   string
   113  		errExpected bool
   114  	}
   115  	testCases := []testcase{
   116  		{
   117  			name:        "successfulCopyFile",
   118  			srcFileName: "file1.log",
   119  			fileDir:     "testdata/zipDir",
   120  			outputDir:   "",
   121  			errExpected: false,
   122  		},
   123  		{
   124  			name:        "failSrcFile",
   125  			srcFileName: "fake_file.log",
   126  			fileDir:     "testdata/zipDir",
   127  			outputDir:   "",
   128  			errExpected: true,
   129  		},
   130  		{
   131  			name:        "failDstFile",
   132  			srcFileName: "file1.log",
   133  			fileDir:     "testdata/zipDir",
   134  			outputDir:   "notarealdir",
   135  			errExpected: true,
   136  		},
   137  	}
   138  	for _, tc := range testCases {
   139  		tcName := tc.name
   140  		tc := tc
   141  		t.Run(tcName, func(t *testing.T) {
   142  			if tc.outputDir == "" {
   143  				tc.outputDir = t.TempDir()
   144  			}
   145  			dstFile := fmt.Sprintf("%s/copiedFile.log", tc.outputDir)
   146  			srcFile := fmt.Sprintf("%s/%s", tc.fileDir, tc.srcFileName)
   147  			err := copyFiles(dstFile, srcFile)
   148  			if tc.errExpected {
   149  				assert.Error(t, err)
   150  			} else {
   151  				assert.NoError(t, err)
   152  				require.NoError(t, err)
   153  				// when there's no error message, we validate that the file was
   154  				// copied correctly
   155  				dstContent, err := os.ReadFile(dstFile)
   156  				require.NoError(t, err)
   157  
   158  				srcContent, err := os.ReadFile(srcFile)
   159  				require.NoError(t, err)
   160  
   161  				assert.Equal(t, string(dstContent), string(srcContent))
   162  			}
   163  		})
   164  	}
   165  }
   166  
   167  func Test_gatherLogsNoK8s(t *testing.T) {
   168  	type testcase struct {
   169  		name       string
   170  		outputFile string
   171  		daemons    string
   172  		errMsg     string
   173  	}
   174  	testCases := []testcase{
   175  		{
   176  			name:       "successfulZipAllDaemonLogs",
   177  			outputFile: "",
   178  			daemons:    "all",
   179  			errMsg:     "",
   180  		},
   181  		{
   182  			name:       "successfulZipOnlyRootLogs",
   183  			outputFile: "",
   184  			daemons:    "root",
   185  			errMsg:     "",
   186  		},
   187  		{
   188  			name:       "successfulZipOnlyConnectorLogs",
   189  			outputFile: "",
   190  			daemons:    "user",
   191  			errMsg:     "",
   192  		},
   193  		{
   194  			name:       "successfulZipNoDaemonLogs",
   195  			outputFile: "",
   196  			daemons:    "None",
   197  			errMsg:     "",
   198  		},
   199  		{
   200  			name:       "incorrectDaemonFlagValue",
   201  			outputFile: "",
   202  			daemons:    "notARealFlagValue",
   203  			errMsg:     "Options for --daemons are: all, root, user, or None",
   204  		},
   205  	}
   206  
   207  	for _, tc := range testCases {
   208  		tcName := tc.name
   209  		tc := tc
   210  		t.Run(tcName, func(t *testing.T) {
   211  			// Use this time to validate that the zip file says the
   212  			// files inside were modified after the test started.
   213  			startTime := time.Now()
   214  			// Prepare the context + use our testdata log dir for these tests
   215  			ctx := dlog.NewTestContext(t, false)
   216  			testLogDir := "testdata/testLogDir"
   217  			ctx = filelocation.WithAppUserLogDir(ctx, testLogDir)
   218  			ctx = connect.WithCommandInitializer(ctx, connect.CommandInitializer)
   219  
   220  			// this isn't actually used for our unit tests, but is needed for the function
   221  			// when it is getting logs from k8s components
   222  			cmd := &cobra.Command{}
   223  
   224  			// override the outputFile
   225  			outputDir := t.TempDir()
   226  			if tc.outputFile == "" {
   227  				tc.outputFile = fmt.Sprintf("%s/telepresence_logs.zip", outputDir)
   228  			}
   229  			stdout := dlog.StdLogger(ctx, dlog.LogLevelInfo).Writer()
   230  			stderr := dlog.StdLogger(ctx, dlog.LogLevelError).Writer()
   231  			cmd.SetOut(stdout)
   232  			cmd.SetErr(stderr)
   233  			cmd.SetContext(ctx)
   234  			gl := &gatherLogsCommand{
   235  				outputFile: tc.outputFile,
   236  				daemons:    tc.daemons,
   237  				// We will test other values of this in our integration tests since
   238  				// they require a kubernetes cluster
   239  				trafficAgents:  "None",
   240  				trafficManager: false,
   241  			}
   242  
   243  			// Ensure we can create a zip of the logs
   244  			err := gl.gatherLogs(cmd, nil)
   245  			if tc.errMsg != "" {
   246  				require.Error(t, err)
   247  				assert.Contains(t, err.Error(), tc.errMsg)
   248  			} else {
   249  				require.NoError(t, err)
   250  
   251  				// Validate that the zip file only contains the files we expect
   252  				zipReader, err := zip.OpenReader(tc.outputFile)
   253  				require.NoError(t, err)
   254  				defer zipReader.Close()
   255  
   256  				var regexStr string
   257  				switch gl.daemons {
   258  				case "all":
   259  					regexStr = "cli|connector|daemon"
   260  				case "root":
   261  					regexStr = "daemon"
   262  				case "user":
   263  					regexStr = "connector"
   264  				case "None":
   265  					regexStr = "a^" // impossible to match
   266  				default:
   267  					// We shouldn't hit this
   268  					t.Fatal("Used an option for daemon that is impossible")
   269  				}
   270  				for _, f := range zipReader.File {
   271  					// Ensure the file was actually supposed to be in the zip
   272  					assert.Regexp(t, regexp.MustCompile(regexStr), f.Name)
   273  
   274  					filesEqual, err := checkZipEqual(f, testLogDir)
   275  					require.NoError(t, err)
   276  					assert.True(t, filesEqual)
   277  
   278  					// Ensure the zip file metadata is correct (e.g. not the
   279  					// default which is 1979) that it was modified after the
   280  					// test started.
   281  					// This test is incredibly fast (within a second) so we
   282  					// convert the times to unix timestamps (to get us to
   283  					// nearest seconds) and ensure the unix timestamp for the
   284  					// zip file is not less than the unix timestamp for the
   285  					// start time.
   286  					// If this ends up being flakey, we can move the start
   287  					// time out of the test loop and add a sleep for a second
   288  					// to ensure nothing weird could happen with rounding.
   289  					assert.False(t,
   290  						f.FileInfo().ModTime().Unix() < startTime.Unix(),
   291  						fmt.Sprintf("Start time: %d, file time: %d",
   292  							startTime.Unix(),
   293  							f.FileInfo().ModTime().Unix()))
   294  				}
   295  			}
   296  		})
   297  	}
   298  }
   299  
   300  func Test_gatherLogsGetPodName(t *testing.T) {
   301  	podNames := []string{
   302  		"echo-auto-inject-64323-3454.default",
   303  		"echo-easy-141245-23432.ambassador",
   304  		"traffic-manager-123214-2332.ambassador",
   305  	}
   306  	podMapping := []string{
   307  		"pod-1.namespace-1",
   308  		"pod-2.namespace-2",
   309  		"traffic-manager.namespace-2",
   310  	}
   311  
   312  	// We need a fresh anonymizer for each test
   313  	anonymizer := &anonymizer{
   314  		namespaces: make(map[string]string),
   315  		podNames:   make(map[string]string),
   316  	}
   317  	// Get the newPodName for each pod
   318  	for _, podName := range podNames {
   319  		newPodName := anonymizer.getPodName(podName)
   320  		require.NotEqual(t, podName, newPodName)
   321  	}
   322  	// Ensure the anonymizer contains the total expected values
   323  	require.Equal(t, 3, len(anonymizer.podNames))
   324  	require.Equal(t, 2, len(anonymizer.namespaces))
   325  
   326  	// Ensure the podNames were anonymized correctly
   327  	for i := range podNames {
   328  		require.Equal(t, podMapping[i], anonymizer.podNames[podNames[i]])
   329  	}
   330  
   331  	// Ensure the namespaces were anonymized correctly
   332  	require.Equal(t, "namespace-1", anonymizer.namespaces["default"])
   333  	require.Equal(t, "namespace-2", anonymizer.namespaces["ambassador"])
   334  }
   335  
   336  func Test_gatherLogsAnonymizeLogs(t *testing.T) {
   337  	anonymizer := &anonymizer{
   338  		namespaces: map[string]string{
   339  			"default":    "namespace-1",
   340  			"ambassador": "namespace-2",
   341  		},
   342  		// these names are specific because they come from the test data
   343  		podNames: map[string]string{
   344  			"echo-auto-inject-6496f77cbd-n86nc.default":   "pod-1.namespace-1",
   345  			"traffic-manager-5c69859f94-g4ntj.ambassador": "traffic-manager.namespace-2",
   346  		},
   347  	}
   348  
   349  	testLogDir := "testdata/testLogDir"
   350  	outputDir := t.TempDir()
   351  	files := []string{"echo-auto-inject-6496f77cbd-n86nc", "traffic-manager-5c69859f94-g4ntj"}
   352  	for _, file := range files {
   353  		// The anonymize function edits files in place
   354  		// so copy the files before we do that
   355  		srcFile := fmt.Sprintf("%s/%s", testLogDir, file)
   356  		dstFile := fmt.Sprintf("%s/%s", outputDir, file)
   357  		err := copyFiles(dstFile, srcFile)
   358  		require.NoError(t, err)
   359  
   360  		err = anonymizer.anonymizeLog(dstFile)
   361  		require.NoError(t, err)
   362  
   363  		// Now verify things have actually been anonymized
   364  		anonFile, err := os.ReadFile(dstFile)
   365  		require.NoError(t, err)
   366  		require.NotContains(t, string(anonFile), "echo-auto-inject")
   367  		require.NotContains(t, string(anonFile), "default")
   368  		require.NotContains(t, string(anonFile), "ambassador")
   369  
   370  		// Both logs make reference to "echo-auto-inject" so we
   371  		// validate that "pod-1" appears in both logs
   372  		require.Contains(t, string(anonFile), "pod-1")
   373  	}
   374  }
   375  
   376  func Test_gatherLogsSignificantPodNames(t *testing.T) {
   377  	type testcase struct {
   378  		name    string
   379  		podName string
   380  		results []string
   381  	}
   382  	testCases := []testcase{
   383  		{
   384  			name:    "deploymentPod",
   385  			podName: "echo-easy-867b648b88-zjsp2",
   386  			results: []string{
   387  				"echo-easy-867b648b88-zjsp2",
   388  				"echo-easy-867b648b88",
   389  				"echo-easy",
   390  			},
   391  		},
   392  		{
   393  			name:    "statefulSetPod",
   394  			podName: "echo-easy-0",
   395  			results: []string{
   396  				"echo-easy-0",
   397  				"echo-easy",
   398  			},
   399  		},
   400  		{
   401  			name:    "unknownName",
   402  			podName: "notarealname",
   403  			results: []string{},
   404  		},
   405  		{
   406  			name:    "followPatternNotFullName",
   407  			podName: "a123b",
   408  			results: []string{},
   409  		},
   410  		{
   411  			name:    "emptyName",
   412  			podName: "",
   413  			results: []string{},
   414  		},
   415  	}
   416  
   417  	for _, tc := range testCases {
   418  		tcName := tc.name
   419  		tc := tc
   420  		// We need a fresh anonymizer for each test
   421  		t.Run(tcName, func(t *testing.T) {
   422  			sigPodNames := getSignificantPodNames(tc.podName)
   423  			require.Equal(t, tc.results, sigPodNames)
   424  		})
   425  	}
   426  }
   427  
   428  // ReadZip reads a zip file and returns the []byte string. Used in tests for
   429  // checking that a zipped file's contents are correct. Exported since it is
   430  // also used in telepresence_test.go.
   431  func ReadZip(zippedFile *zip.File) ([]byte, error) {
   432  	fileReader, err := zippedFile.Open()
   433  	if err != nil {
   434  		return nil, err
   435  	}
   436  
   437  	fileContent, err := io.ReadAll(fileReader)
   438  	if err != nil {
   439  		return nil, err
   440  	}
   441  	return fileContent, nil
   442  }
   443  
   444  // checkZipEqual is a helper function for validating that the zippedFile in the
   445  // zip directory matches the file that was used to create the zip.
   446  func checkZipEqual(zippedFile *zip.File, srcLogDir string) (bool, error) {
   447  	dstContent, err := ReadZip(zippedFile)
   448  	if err != nil {
   449  		return false, err
   450  	}
   451  	srcContent, err := os.ReadFile(fmt.Sprintf("%s/%s", srcLogDir, zippedFile.Name))
   452  	if err != nil {
   453  		return false, err
   454  	}
   455  
   456  	return string(dstContent) == string(srcContent), nil
   457  }