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 }