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 }