k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/pkg/kubelet/logs/container_log_manager_test.go (about) 1 /* 2 Copyright 2018 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 logs 18 19 import ( 20 "bytes" 21 "context" 22 "fmt" 23 "io" 24 "os" 25 "path/filepath" 26 "sync" 27 "testing" 28 "time" 29 30 "github.com/stretchr/testify/assert" 31 "github.com/stretchr/testify/require" 32 v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 33 "k8s.io/apimachinery/pkg/util/wait" 34 "k8s.io/client-go/util/workqueue" 35 "k8s.io/kubernetes/pkg/kubelet/container" 36 37 runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1" 38 critest "k8s.io/cri-api/pkg/apis/testing" 39 testingclock "k8s.io/utils/clock/testing" 40 ) 41 42 func TestGetAllLogs(t *testing.T) { 43 dir, err := os.MkdirTemp("", "test-get-all-logs") 44 require.NoError(t, err) 45 defer os.RemoveAll(dir) 46 testLogs := []string{ 47 "test-log.11111111-111111.gz", 48 "test-log", 49 "test-log.00000000-000000.gz", 50 "test-log.19900322-000000.gz", 51 "test-log.19900322-111111.gz", 52 "test-log.19880620-000000", // unused log 53 "test-log.19880620-000000.gz", 54 "test-log.19880620-111111.gz", 55 "test-log.20180101-000000", 56 "test-log.20180101-000000.tmp", // temporary log 57 } 58 expectLogs := []string{ 59 "test-log.00000000-000000.gz", 60 "test-log.11111111-111111.gz", 61 "test-log.19880620-000000.gz", 62 "test-log.19880620-111111.gz", 63 "test-log.19900322-000000.gz", 64 "test-log.19900322-111111.gz", 65 "test-log.20180101-000000", 66 "test-log", 67 } 68 for i := range testLogs { 69 f, err := os.Create(filepath.Join(dir, testLogs[i])) 70 require.NoError(t, err) 71 f.Close() 72 } 73 got, err := GetAllLogs(filepath.Join(dir, "test-log")) 74 assert.NoError(t, err) 75 for i := range expectLogs { 76 expectLogs[i] = filepath.Join(dir, expectLogs[i]) 77 } 78 assert.Equal(t, expectLogs, got) 79 } 80 81 func TestRotateLogs(t *testing.T) { 82 ctx := context.Background() 83 dir, err := os.MkdirTemp("", "test-rotate-logs") 84 require.NoError(t, err) 85 defer os.RemoveAll(dir) 86 87 const ( 88 testMaxFiles = 3 89 testMaxSize = 10 90 ) 91 now := time.Now() 92 f := critest.NewFakeRuntimeService() 93 c := &containerLogManager{ 94 runtimeService: f, 95 policy: LogRotatePolicy{ 96 MaxSize: testMaxSize, 97 MaxFiles: testMaxFiles, 98 }, 99 osInterface: container.RealOS{}, 100 clock: testingclock.NewFakeClock(now), 101 mutex: sync.Mutex{}, 102 queue: workqueue.NewTypedRateLimitingQueueWithConfig( 103 workqueue.DefaultTypedControllerRateLimiter[string](), 104 workqueue.TypedRateLimitingQueueConfig[string]{Name: "kubelet_log_rotate_manager"}, 105 ), 106 maxWorkers: 10, 107 monitoringPeriod: v1.Duration{Duration: 10 * time.Second}, 108 } 109 testLogs := []string{ 110 "test-log-1", 111 "test-log-2", 112 "test-log-3", 113 "test-log-4", 114 "test-log-3.00000000-000001", 115 "test-log-3.00000000-000000.gz", 116 } 117 testContent := []string{ 118 "short", 119 "longer than 10 bytes", 120 "longer than 10 bytes", 121 "longer than 10 bytes", 122 "the length doesn't matter", 123 "the length doesn't matter", 124 } 125 for i := range testLogs { 126 f, err := os.Create(filepath.Join(dir, testLogs[i])) 127 require.NoError(t, err) 128 _, err = f.Write([]byte(testContent[i])) 129 require.NoError(t, err) 130 f.Close() 131 } 132 testContainers := []*critest.FakeContainer{ 133 { 134 ContainerStatus: runtimeapi.ContainerStatus{ 135 Id: "container-not-need-rotate", 136 State: runtimeapi.ContainerState_CONTAINER_RUNNING, 137 LogPath: filepath.Join(dir, testLogs[0]), 138 }, 139 }, 140 { 141 ContainerStatus: runtimeapi.ContainerStatus{ 142 Id: "container-need-rotate", 143 State: runtimeapi.ContainerState_CONTAINER_RUNNING, 144 LogPath: filepath.Join(dir, testLogs[1]), 145 }, 146 }, 147 { 148 ContainerStatus: runtimeapi.ContainerStatus{ 149 Id: "container-has-excess-log", 150 State: runtimeapi.ContainerState_CONTAINER_RUNNING, 151 LogPath: filepath.Join(dir, testLogs[2]), 152 }, 153 }, 154 { 155 ContainerStatus: runtimeapi.ContainerStatus{ 156 Id: "container-is-not-running", 157 State: runtimeapi.ContainerState_CONTAINER_EXITED, 158 LogPath: filepath.Join(dir, testLogs[3]), 159 }, 160 }, 161 } 162 f.SetFakeContainers(testContainers) 163 164 // Push the items into the queue for before starting the worker to avoid issue with the queue being empty. 165 require.NoError(t, c.rotateLogs(ctx)) 166 167 // Start a routine that can monitor the queue and shutdown the queue to trigger the retrun from the processQueueItems 168 // Keeping the monitor duration smaller in order to keep the unwanted delay in the test to a minimal. 169 go func() { 170 pollTimeoutCtx, cancel := context.WithTimeout(ctx, 10*time.Second) 171 defer cancel() 172 err = wait.PollUntilContextCancel(pollTimeoutCtx, 5*time.Millisecond, false, func(ctx context.Context) (done bool, err error) { 173 return c.queue.Len() == 0, nil 174 }) 175 require.NoError(t, err) 176 c.queue.ShutDown() 177 }() 178 // This is a blocking call. But the above routine takes care of ensuring that this is terminated once the queue is shutdown 179 c.processQueueItems(ctx, 1) 180 181 timestamp := now.Format(timestampFormat) 182 logs, err := os.ReadDir(dir) 183 require.NoError(t, err) 184 assert.Len(t, logs, 5) 185 assert.Equal(t, testLogs[0], logs[0].Name()) 186 assert.Equal(t, testLogs[1]+"."+timestamp, logs[1].Name()) 187 assert.Equal(t, testLogs[4]+compressSuffix, logs[2].Name()) 188 assert.Equal(t, testLogs[2]+"."+timestamp, logs[3].Name()) 189 assert.Equal(t, testLogs[3], logs[4].Name()) 190 } 191 192 func TestClean(t *testing.T) { 193 ctx := context.Background() 194 dir, err := os.MkdirTemp("", "test-clean") 195 require.NoError(t, err) 196 defer os.RemoveAll(dir) 197 198 const ( 199 testMaxFiles = 3 200 testMaxSize = 10 201 ) 202 now := time.Now() 203 f := critest.NewFakeRuntimeService() 204 c := &containerLogManager{ 205 runtimeService: f, 206 policy: LogRotatePolicy{ 207 MaxSize: testMaxSize, 208 MaxFiles: testMaxFiles, 209 }, 210 osInterface: container.RealOS{}, 211 clock: testingclock.NewFakeClock(now), 212 mutex: sync.Mutex{}, 213 queue: workqueue.NewTypedRateLimitingQueueWithConfig( 214 workqueue.DefaultTypedControllerRateLimiter[string](), 215 workqueue.TypedRateLimitingQueueConfig[string]{Name: "kubelet_log_rotate_manager"}, 216 ), 217 maxWorkers: 10, 218 monitoringPeriod: v1.Duration{Duration: 10 * time.Second}, 219 } 220 testLogs := []string{ 221 "test-log-1", 222 "test-log-2", 223 "test-log-3", 224 "test-log-2.00000000-000000.gz", 225 "test-log-2.00000000-000001", 226 "test-log-3.00000000-000000.gz", 227 "test-log-3.00000000-000001", 228 } 229 for i := range testLogs { 230 f, err := os.Create(filepath.Join(dir, testLogs[i])) 231 require.NoError(t, err) 232 f.Close() 233 } 234 testContainers := []*critest.FakeContainer{ 235 { 236 ContainerStatus: runtimeapi.ContainerStatus{ 237 Id: "container-1", 238 State: runtimeapi.ContainerState_CONTAINER_RUNNING, 239 LogPath: filepath.Join(dir, testLogs[0]), 240 }, 241 }, 242 { 243 ContainerStatus: runtimeapi.ContainerStatus{ 244 Id: "container-2", 245 State: runtimeapi.ContainerState_CONTAINER_RUNNING, 246 LogPath: filepath.Join(dir, testLogs[1]), 247 }, 248 }, 249 { 250 ContainerStatus: runtimeapi.ContainerStatus{ 251 Id: "container-3", 252 State: runtimeapi.ContainerState_CONTAINER_EXITED, 253 LogPath: filepath.Join(dir, testLogs[2]), 254 }, 255 }, 256 } 257 f.SetFakeContainers(testContainers) 258 259 err = c.Clean(ctx, "container-3") 260 require.NoError(t, err) 261 262 logs, err := os.ReadDir(dir) 263 require.NoError(t, err) 264 assert.Len(t, logs, 4) 265 assert.Equal(t, testLogs[0], logs[0].Name()) 266 assert.Equal(t, testLogs[1], logs[1].Name()) 267 assert.Equal(t, testLogs[3], logs[2].Name()) 268 assert.Equal(t, testLogs[4], logs[3].Name()) 269 } 270 271 func TestCleanupUnusedLog(t *testing.T) { 272 dir, err := os.MkdirTemp("", "test-cleanup-unused-log") 273 require.NoError(t, err) 274 defer os.RemoveAll(dir) 275 276 testLogs := []string{ 277 "test-log-1", // regular log 278 "test-log-1.tmp", // temporary log 279 "test-log-2", // unused log 280 "test-log-2.gz", // compressed log 281 } 282 283 for i := range testLogs { 284 testLogs[i] = filepath.Join(dir, testLogs[i]) 285 f, err := os.Create(testLogs[i]) 286 require.NoError(t, err) 287 f.Close() 288 } 289 290 c := &containerLogManager{ 291 osInterface: container.RealOS{}, 292 } 293 got, err := c.cleanupUnusedLogs(testLogs) 294 require.NoError(t, err) 295 assert.Len(t, got, 2) 296 assert.Equal(t, []string{testLogs[0], testLogs[3]}, got) 297 298 logs, err := os.ReadDir(dir) 299 require.NoError(t, err) 300 assert.Len(t, logs, 2) 301 assert.Equal(t, testLogs[0], filepath.Join(dir, logs[0].Name())) 302 assert.Equal(t, testLogs[3], filepath.Join(dir, logs[1].Name())) 303 } 304 305 func TestRemoveExcessLog(t *testing.T) { 306 for desc, test := range map[string]struct { 307 max int 308 expect []string 309 }{ 310 "MaxFiles equal to 2": { 311 max: 2, 312 expect: []string{}, 313 }, 314 "MaxFiles more than 2": { 315 max: 3, 316 expect: []string{"test-log-4"}, 317 }, 318 "MaxFiles more than log file number": { 319 max: 6, 320 expect: []string{"test-log-1", "test-log-2", "test-log-3", "test-log-4"}, 321 }, 322 } { 323 t.Logf("TestCase %q", desc) 324 dir, err := os.MkdirTemp("", "test-remove-excess-log") 325 require.NoError(t, err) 326 defer os.RemoveAll(dir) 327 328 testLogs := []string{"test-log-3", "test-log-1", "test-log-2", "test-log-4"} 329 330 for i := range testLogs { 331 testLogs[i] = filepath.Join(dir, testLogs[i]) 332 f, err := os.Create(testLogs[i]) 333 require.NoError(t, err) 334 f.Close() 335 } 336 337 c := &containerLogManager{ 338 policy: LogRotatePolicy{MaxFiles: test.max}, 339 osInterface: container.RealOS{}, 340 } 341 got, err := c.removeExcessLogs(testLogs) 342 require.NoError(t, err) 343 require.Len(t, got, len(test.expect)) 344 for i, name := range test.expect { 345 assert.Equal(t, name, filepath.Base(got[i])) 346 } 347 348 logs, err := os.ReadDir(dir) 349 require.NoError(t, err) 350 require.Len(t, logs, len(test.expect)) 351 for i, name := range test.expect { 352 assert.Equal(t, name, logs[i].Name()) 353 } 354 } 355 } 356 357 func TestCompressLog(t *testing.T) { 358 dir, err := os.MkdirTemp("", "test-compress-log") 359 require.NoError(t, err) 360 defer os.RemoveAll(dir) 361 362 testFile, err := os.CreateTemp(dir, "test-rotate-latest-log") 363 require.NoError(t, err) 364 defer testFile.Close() 365 testContent := "test log content" 366 _, err = testFile.Write([]byte(testContent)) 367 require.NoError(t, err) 368 testFile.Close() 369 370 testLog := testFile.Name() 371 c := &containerLogManager{osInterface: container.RealOS{}} 372 require.NoError(t, c.compressLog(testLog)) 373 _, err = os.Stat(testLog + compressSuffix) 374 assert.NoError(t, err, "log should be compressed") 375 _, err = os.Stat(testLog + tmpSuffix) 376 assert.Error(t, err, "temporary log should be renamed") 377 _, err = os.Stat(testLog) 378 assert.Error(t, err, "original log should be removed") 379 380 rc, err := UncompressLog(testLog + compressSuffix) 381 require.NoError(t, err) 382 defer rc.Close() 383 var buf bytes.Buffer 384 _, err = io.Copy(&buf, rc) 385 require.NoError(t, err) 386 assert.Equal(t, testContent, buf.String()) 387 } 388 389 func TestRotateLatestLog(t *testing.T) { 390 ctx := context.Background() 391 dir, err := os.MkdirTemp("", "test-rotate-latest-log") 392 require.NoError(t, err) 393 defer os.RemoveAll(dir) 394 395 for desc, test := range map[string]struct { 396 runtimeError error 397 maxFiles int 398 expectError bool 399 expectOriginal bool 400 expectRotated bool 401 }{ 402 "should successfully rotate log when MaxFiles is 2": { 403 maxFiles: 2, 404 expectError: false, 405 expectOriginal: false, 406 expectRotated: true, 407 }, 408 "should restore original log when ReopenContainerLog fails": { 409 runtimeError: fmt.Errorf("random error"), 410 maxFiles: 2, 411 expectError: true, 412 expectOriginal: true, 413 expectRotated: false, 414 }, 415 } { 416 t.Logf("TestCase %q", desc) 417 now := time.Now() 418 f := critest.NewFakeRuntimeService() 419 c := &containerLogManager{ 420 runtimeService: f, 421 policy: LogRotatePolicy{MaxFiles: test.maxFiles}, 422 osInterface: container.RealOS{}, 423 clock: testingclock.NewFakeClock(now), 424 mutex: sync.Mutex{}, 425 queue: workqueue.NewTypedRateLimitingQueueWithConfig( 426 workqueue.DefaultTypedControllerRateLimiter[string](), 427 workqueue.TypedRateLimitingQueueConfig[string]{Name: "kubelet_log_rotate_manager"}, 428 ), 429 maxWorkers: 10, 430 monitoringPeriod: v1.Duration{Duration: 10 * time.Second}, 431 } 432 if test.runtimeError != nil { 433 f.InjectError("ReopenContainerLog", test.runtimeError) 434 } 435 testFile, err := os.CreateTemp(dir, "test-rotate-latest-log") 436 require.NoError(t, err) 437 testFile.Close() 438 defer testFile.Close() 439 testLog := testFile.Name() 440 rotatedLog := fmt.Sprintf("%s.%s", testLog, now.Format(timestampFormat)) 441 err = c.rotateLatestLog(ctx, "test-id", testLog) 442 assert.Equal(t, test.expectError, err != nil) 443 _, err = os.Stat(testLog) 444 assert.Equal(t, test.expectOriginal, err == nil) 445 _, err = os.Stat(rotatedLog) 446 assert.Equal(t, test.expectRotated, err == nil) 447 assert.NoError(t, f.AssertCalls([]string{"ReopenContainerLog"})) 448 } 449 }