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