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  }