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  }