knative.dev/pkg@v0.0.0-20260602142205-ac97e43f6622/test/logstream/v2/stream_test.go (about)

     1  /*
     2  Copyright 2020 The Knative 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 logstream_test
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"net/http"
    25  	"strings"
    26  	"testing"
    27  	"time"
    28  
    29  	corev1 "k8s.io/api/core/v1"
    30  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    31  	"k8s.io/apimachinery/pkg/runtime/schema"
    32  	"k8s.io/apimachinery/pkg/util/sets"
    33  	"k8s.io/apimachinery/pkg/util/wait"
    34  	"k8s.io/apimachinery/pkg/watch"
    35  	"k8s.io/client-go/kubernetes/fake"
    36  	"k8s.io/client-go/kubernetes/scheme"
    37  	v1 "k8s.io/client-go/kubernetes/typed/core/v1"
    38  	fakecorev1 "k8s.io/client-go/kubernetes/typed/core/v1/fake"
    39  	restclient "k8s.io/client-go/rest"
    40  	fakerest "k8s.io/client-go/rest/fake"
    41  	"knative.dev/pkg/test/logstream/v2"
    42  )
    43  
    44  const (
    45  	knativeContainer = "knativeContainer"
    46  	userContainer    = "userContainer"
    47  	noLogTimeout     = 100 * time.Millisecond
    48  	testKey          = "horror-movie-2020"
    49  	// default test controller line with all matchin keys and attributes
    50  	testLine = `{"severity":"debug","timestamp":"2020-10-20T18:42:28.553Z","logger":"controller.revision-controller.knative.dev-serving-pkg-reconciler-revision.Reconciler","caller":"controller/controller.go:397","message":"Adding to queue default/s2-nhjv6 (depth: 1)","commit":"4411bf3","knative.dev/pod":"controller-f95b977c-4wlh4","knative.dev/controller":"revision-controller","knative.dev/key":"default/horror-movie-2020", "error":"el-otoño-eternal" }`
    51  
    52  	// test controller line with mismatched key entry (knative.dev/key)
    53  	testLineWithMissmatchedKey = `{"severity":"debug","timestamp":"2020-10-20T18:42:28.553Z","logger":"controller.revision-controller.knative.dev-serving-pkg-reconciler-revision.Reconciler","caller":"controller/controller.go:397","message":"Adding to queue default/s2-nhjv6 (depth: 1)","commit":"4411bf3","knative.dev/pod":"controller-f95b977c-4wlh4","knative.dev/controller":"revision-controller","knative.dev/key":"default/romcom-1990", "error":"el-otoño-eternal" }`
    54  
    55  	// test controller line with missing key entry (knative.dev/key)
    56  	testLineWithMissingKey = `{"severity":"debug","timestamp":"2020-10-20T18:42:28.553Z","logger":"controller.revision-controller.knative.dev-serving-pkg-reconciler-revision.Reconciler","caller":"controller/controller.go:397","message":"Adding to queue default/s2-nhjv6 (depth: 1)","commit":"4411bf3","knative.dev/pod":"controller-f95b977c-4wlh4","knative.dev/controller":"revision-controller", "error":"el-otoño-eternal" }`
    57  
    58  	testNonJSONLine = `Some non-json string produced by controller`
    59  
    60  	// this line doesn't have json entry for knative.dev/controller so we expect
    61  	// log parsing s to fallback to using "caller" attribute.
    62  	testNonControllerLine = `{"severity":"debug","timestamp":"2020-10-20T18:42:28.553Z","logger":"controller.revision-controller.knative.dev-serving-pkg-reconciler-revision.Reconciler","caller":"non_controller.go:397","message":"Adding to queue default/s2-nhjv6 (depth: 1)","commit":"4411bf3","knative.dev/pod":"controller-f95b977c-4wlh4","knative.dev/key":"default/horror-movie-2020", "error":"non_controller_error" }`
    63  
    64  	testChaosDuckLine     = `Some non-json Chaos Duck string`
    65  	testQueueProxyLine    = `Some non-json Queueproxy string`
    66  	testUserContainerLine = `Some non-json user container string`
    67  
    68  	testLinePattern              = "el-otoño-eternal"
    69  	testNonControllerLinePattern = "non_controller_error"
    70  )
    71  
    72  // This map determines test log lines to be produced by each fake container
    73  var (
    74  	logProductionMap = map[string][]string{
    75  		knativeContainer:     {testLine, testLineWithMissmatchedKey, testLineWithMissingKey, testNonJSONLine, testNonControllerLine},
    76  		logstream.ChaosDuck:  {testChaosDuckLine},
    77  		logstream.QueueProxy: {testQueueProxyLine},
    78  		userContainer:        {testUserContainerLine},
    79  	}
    80  
    81  	singlePod = &corev1.Pod{
    82  		ObjectMeta: metav1.ObjectMeta{
    83  			Name:      "RandomPodName",
    84  			Namespace: "defaultNameSpace",
    85  		},
    86  		Spec: corev1.PodSpec{
    87  			Containers: []corev1.Container{{
    88  				Name: knativeContainer,
    89  			}},
    90  		},
    91  	}
    92  
    93  	knativePod = &corev1.Pod{
    94  		ObjectMeta: metav1.ObjectMeta{
    95  			Name:      "RandomPodName",
    96  			Namespace: "defaultNameSpace",
    97  		},
    98  		Spec: corev1.PodSpec{
    99  			Containers: []corev1.Container{{
   100  				Name: knativeContainer,
   101  			}, {
   102  				Name: logstream.ChaosDuck,
   103  			}},
   104  		},
   105  	}
   106  )
   107  
   108  var userPod = &corev1.Pod{
   109  	ObjectMeta: metav1.ObjectMeta{
   110  		Name:      "SomeOtherRandomPodName",
   111  		Namespace: "usertestNamespace",
   112  	},
   113  	Spec: corev1.PodSpec{
   114  		Containers: []corev1.Container{{
   115  			Name: logstream.QueueProxy,
   116  		}, {
   117  			Name: userContainer,
   118  		}},
   119  	},
   120  }
   121  
   122  var readyStatus = corev1.PodStatus{
   123  	Phase: corev1.PodRunning,
   124  	Conditions: []corev1.PodCondition{{
   125  		Type:   corev1.PodReady,
   126  		Status: corev1.ConditionTrue,
   127  	}},
   128  }
   129  
   130  func TestWatchErr(t *testing.T) {
   131  	f := newK8sFake(fake.NewSimpleClientset(), errors.New("lookin' good"), nil)
   132  	stream := logstream.FromNamespace(context.Background(), f, "a-namespace")
   133  	_, err := stream.StartStream(knativePod.Name, nil)
   134  	if err == nil {
   135  		t.Fatal("LogStream creation should have failed")
   136  	}
   137  }
   138  
   139  func TestFailToStartStream(t *testing.T) {
   140  	singlePod := singlePod.DeepCopy()
   141  	singlePod.Status = readyStatus
   142  
   143  	const want = "hungry for apples"
   144  	f := newK8sFake(fake.NewSimpleClientset(), nil, /*watcher*/
   145  		errors.New(want) /*getlogs err*/)
   146  
   147  	logFuncInvoked := make(chan struct{})
   148  	logFunc := func(format string, args ...interface{}) {
   149  		res := fmt.Sprintf(format, args...)
   150  		if !strings.Contains(res, want) {
   151  			t.Errorf("Expected message to contain %q, but message was: %s", want, res)
   152  		}
   153  		close(logFuncInvoked)
   154  	}
   155  	ctx, cancel := context.WithCancel(context.Background())
   156  	stream := logstream.FromNamespace(ctx, f, singlePod.Namespace)
   157  	streamC, err := stream.StartStream(singlePod.Name, logFunc)
   158  	if err != nil {
   159  		t.Fatal("Failed to start the stream: ", err)
   160  	}
   161  	t.Cleanup(func() {
   162  		streamC()
   163  		cancel()
   164  	})
   165  	podClient := f.CoreV1().Pods(singlePod.Namespace)
   166  	if _, err := podClient.Create(context.Background(), singlePod, metav1.CreateOptions{}); err != nil {
   167  		t.Fatal("CreatePod()=", err)
   168  	}
   169  
   170  	select {
   171  	case <-time.After(noLogTimeout):
   172  		t.Error("Timed-out waiting for the logs")
   173  	case <-logFuncInvoked:
   174  	}
   175  }
   176  
   177  func processLogEntries(t *testing.T, logFuncInvoked <-chan string, patterns []string) {
   178  	expectedLogMatchesSet := sets.NewString(patterns...)
   179  
   180  OUTER:
   181  	for len(expectedLogMatchesSet) > 0 {
   182  		// we expect exactly len(expectedLogMatchesSet) log entries
   183  		// each need to be matched with exactly one pattern from
   184  		// patterns...
   185  		select {
   186  		case <-time.After(noLogTimeout):
   187  			t.Error("Timed out: log message wasn't received")
   188  		case logLine := <-logFuncInvoked:
   189  
   190  			// classify string that we got here
   191  			for _, s := range sets.StringKeySet(expectedLogMatchesSet).List() {
   192  				if strings.Contains(logLine, s) {
   193  					expectedLogMatchesSet.Delete(s)
   194  					continue OUTER
   195  				}
   196  			}
   197  			t.Fatal("Unexpected log entry received:", logLine)
   198  		}
   199  	}
   200  
   201  	// now we expected timeout without any logs
   202  	select {
   203  	case <-time.After(noLogTimeout):
   204  	case logLine := <-logFuncInvoked:
   205  		t.Fatal("No more logs expected at this point, got:", logLine)
   206  	}
   207  }
   208  
   209  func TestNamespaceStream(t *testing.T) {
   210  	knativePod := knativePod.DeepCopy() // Needed to run the test multiple times in a row
   211  	userPod := userPod.DeepCopy()
   212  
   213  	f := newK8sFake(fake.NewSimpleClientset(), nil, nil)
   214  
   215  	logFuncInvoked := make(chan string)
   216  	t.Cleanup(func() { close(logFuncInvoked) })
   217  	logFunc := func(format string, args ...interface{}) {
   218  		logFuncInvoked <- fmt.Sprintf(format, args...)
   219  	}
   220  
   221  	ctx, cancel := context.WithCancel(context.Background())
   222  	stream := logstream.New(ctx, f, logstream.WithNamespaces(knativePod.Namespace, userPod.Namespace))
   223  	streamC, err := stream.StartStream(testKey, logFunc)
   224  	if err != nil {
   225  		t.Fatal("Failed to start the stream: ", err)
   226  	}
   227  	t.Cleanup(streamC)
   228  
   229  	podClient := f.CoreV1().Pods(knativePod.Namespace)
   230  	if _, err := podClient.Create(context.Background(), knativePod, metav1.CreateOptions{}); err != nil {
   231  		t.Fatal("CreatePod()=", err)
   232  	}
   233  	userPodClient := f.CoreV1().Pods(userPod.Namespace)
   234  	if _, err := userPodClient.Create(context.Background(), userPod, metav1.CreateOptions{}); err != nil {
   235  		t.Fatal("CreatePod()=", err)
   236  	}
   237  
   238  	select {
   239  	case <-time.After(noLogTimeout):
   240  	case <-logFuncInvoked:
   241  		t.Error("Unready pod should not report logs")
   242  	}
   243  
   244  	knativePod.Status = readyStatus
   245  	if _, err := podClient.Update(context.Background(), knativePod, metav1.UpdateOptions{}); err != nil {
   246  		t.Fatal("UpdatePod()=", err)
   247  	}
   248  	userPod.Status = readyStatus
   249  	if _, err := userPodClient.Update(context.Background(), userPod, metav1.UpdateOptions{}); err != nil {
   250  		t.Fatal("UpdatePod()=", err)
   251  	}
   252  
   253  	// We are expecting to get back 4 log entries:
   254  	//    1. non filtered non json entries from queueproxy
   255  	//    2. non filtered non json entries from chaosduck
   256  	//    3. nicely formatted, filtered(with matching key) entry from knativeContainer
   257  	//    4. nicely formatted, filtered(with matching key) entry from knativeContainer (fallback to caller attribubute)
   258  	processLogEntries(t, logFuncInvoked, []string{testLinePattern, testNonControllerLinePattern, testChaosDuckLine, testQueueProxyLine})
   259  
   260  	if _, err := podClient.Update(context.Background(), knativePod, metav1.UpdateOptions{}); err != nil {
   261  		t.Fatal("UpdatePod()=", err)
   262  	}
   263  	if _, err := userPodClient.Update(context.Background(), userPod, metav1.UpdateOptions{}); err != nil {
   264  		t.Fatal("UpdatePod()=", err)
   265  	}
   266  
   267  	select {
   268  	case <-time.After(noLogTimeout):
   269  	case <-logFuncInvoked:
   270  		t.Error("Repeat updates to the same pod should not trigger GetLogs")
   271  	}
   272  
   273  	if err := podClient.Delete(context.Background(), knativePod.Name, metav1.DeleteOptions{}); err != nil {
   274  		t.Fatal("UpdatePod()=", err)
   275  	}
   276  	if err := userPodClient.Delete(context.Background(), userPod.Name, metav1.DeleteOptions{}); err != nil {
   277  		t.Fatal("UpdatePod()=", err)
   278  	}
   279  
   280  	select {
   281  	case <-time.After(noLogTimeout):
   282  	case <-logFuncInvoked:
   283  		t.Error("Deletion should not trigger GetLogs")
   284  	}
   285  
   286  	knativePod.Spec.Containers[0].Name = "goose-with-a-flair"
   287  	// Create pod with the same name? Why not. And let's make it ready from the get go.
   288  	if _, err := podClient.Create(context.Background(), knativePod, metav1.CreateOptions{}); err != nil {
   289  		t.Fatal("CreatePod()=", err)
   290  	}
   291  
   292  	select {
   293  	case <-time.After(noLogTimeout):
   294  		t.Error("Timed out: log message wasn't received")
   295  	case <-logFuncInvoked:
   296  	}
   297  
   298  	// Delete again.
   299  	if err := podClient.Delete(context.Background(), knativePod.Name, metav1.DeleteOptions{}); err != nil {
   300  		t.Fatal("UpdatePod()=", err)
   301  	}
   302  	// Kill the context.
   303  	cancel()
   304  
   305  	// We can't assume that the cancel signal doesn't race the pod creation signal, so
   306  	// we retry a few times to give some leeway.
   307  	pollCtx := context.Background()
   308  	if err := wait.PollUntilContextTimeout(pollCtx, 10*time.Millisecond, time.Second, true, func(ctx context.Context) (bool, error) {
   309  		if _, err := podClient.Create(pollCtx, knativePod, metav1.CreateOptions{}); err != nil {
   310  			return false, err
   311  		}
   312  
   313  		select {
   314  		case <-time.After(noLogTimeout):
   315  			return true, nil
   316  		case <-logFuncInvoked:
   317  			t.Log("Log was still produced, trying again...")
   318  			if err := podClient.Delete(pollCtx, knativePod.Name, metav1.DeleteOptions{}); err != nil {
   319  				return false, err
   320  			}
   321  			return false, nil
   322  		}
   323  	}); err != nil {
   324  		t.Fatal("No watching should have happened", err)
   325  	}
   326  }
   327  
   328  func newK8sFake(c *fake.Clientset, watchErr, logsErr error) *fakeclient {
   329  	return &fakeclient{
   330  		Clientset:  c,
   331  		FakeCoreV1: &fakecorev1.FakeCoreV1{Fake: &c.Fake},
   332  		watchErr:   watchErr,
   333  		logsErr:    logsErr,
   334  	}
   335  }
   336  
   337  type fakeclient struct {
   338  	*fake.Clientset
   339  	*fakecorev1.FakeCoreV1
   340  	watchErr error
   341  	logsErr  error
   342  }
   343  
   344  type fakePods struct {
   345  	*fakeclient
   346  	v1.PodInterface
   347  	ns       string
   348  	watchErr error
   349  	logsErr  error
   350  }
   351  
   352  func (f *fakePods) Watch(ctx context.Context, lo metav1.ListOptions) (watch.Interface, error) {
   353  	if f.watchErr == nil {
   354  		return f.PodInterface.Watch(ctx, lo)
   355  	}
   356  	return nil, f.watchErr
   357  }
   358  
   359  func (f *fakeclient) CoreV1() v1.CoreV1Interface { return f }
   360  
   361  func (f *fakeclient) Pods(ns string) v1.PodInterface {
   362  	return &fakePods{
   363  		f,
   364  		f.FakeCoreV1.Pods(ns),
   365  		ns,
   366  		f.watchErr,
   367  		f.logsErr,
   368  	}
   369  }
   370  
   371  func logsForContainer(container string) string {
   372  	result := ""
   373  
   374  	for _, s := range logProductionMap[container] {
   375  		if len(result) > 0 {
   376  			result += "\n"
   377  		}
   378  		result += s
   379  	}
   380  	return result
   381  }
   382  
   383  func (f *fakePods) GetLogs(podName string, opts *corev1.PodLogOptions) *restclient.Request {
   384  	fakeClient := &fakerest.RESTClient{
   385  		Client: fakerest.CreateHTTPClient(func(request *http.Request) (*http.Response, error) {
   386  			resp := &http.Response{
   387  				StatusCode: http.StatusOK,
   388  				Body: io.NopCloser(
   389  					strings.NewReader(logsForContainer(opts.Container))),
   390  			}
   391  			return resp, nil
   392  		}),
   393  		NegotiatedSerializer: scheme.Codecs.WithoutConversion(),
   394  		GroupVersion:         schema.GroupVersion{Version: "v1"},
   395  		VersionedAPIPath:     fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/log", f.ns, podName),
   396  	}
   397  	ret := fakeClient.Request()
   398  	if f.logsErr != nil {
   399  		ret.Body(f.logsErr)
   400  	}
   401  	return ret
   402  }