sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/crier/reporters/gcs/kubernetes/reporter_test.go (about) 1 /* 2 Copyright 2020 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 kubernetes 18 19 import ( 20 "context" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "io" 25 "testing" 26 "time" 27 28 "github.com/google/go-cmp/cmp" 29 "github.com/sirupsen/logrus" 30 v1 "k8s.io/api/core/v1" 31 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 32 "k8s.io/apimachinery/pkg/types" 33 "sigs.k8s.io/controller-runtime/pkg/reconcile" 34 "sigs.k8s.io/prow/pkg/config" 35 36 "sigs.k8s.io/prow/pkg/io/fakeopener" 37 38 prowv1 "sigs.k8s.io/prow/pkg/apis/prowjobs/v1" 39 ) 40 41 type fca struct { 42 c config.Config 43 } 44 45 func (ca fca) Config() *config.Config { 46 return &ca.c 47 } 48 49 func TestShouldReport(t *testing.T) { 50 tests := []struct { 51 name string 52 agent prowv1.ProwJobAgent 53 isComplete bool 54 hasNoPendingTimestamp bool 55 hasBuildID bool 56 shouldReport bool 57 }{ 58 { 59 name: "completed kubernetes tests are reported", 60 agent: prowv1.KubernetesAgent, 61 isComplete: true, 62 hasBuildID: true, 63 shouldReport: true, 64 }, 65 { 66 name: "pending job is reported", 67 agent: prowv1.KubernetesAgent, 68 isComplete: false, 69 hasBuildID: true, 70 shouldReport: true, 71 }, 72 { 73 name: "not yet pending job is not reported", 74 agent: prowv1.KubernetesAgent, 75 isComplete: false, 76 hasNoPendingTimestamp: true, 77 hasBuildID: true, 78 shouldReport: false, 79 }, 80 { 81 name: "complete non-kubernetes tests are not reported", 82 agent: prowv1.JenkinsAgent, 83 isComplete: true, 84 hasBuildID: true, 85 shouldReport: false, 86 }, 87 { 88 name: "incomplete non-kubernetes tests are not reported", 89 agent: prowv1.JenkinsAgent, 90 isComplete: false, 91 hasBuildID: true, 92 shouldReport: false, 93 }, 94 { 95 name: "complete kubernetes tests with no build ID are not reported", 96 agent: prowv1.KubernetesAgent, 97 isComplete: true, 98 hasBuildID: false, 99 shouldReport: false, 100 }, 101 } 102 103 for _, tc := range tests { 104 t.Run(tc.name, func(t *testing.T) { 105 pj := &prowv1.ProwJob{ 106 Spec: prowv1.ProwJobSpec{ 107 Agent: tc.agent, 108 }, 109 Status: prowv1.ProwJobStatus{ 110 State: prowv1.PendingState, 111 StartTime: metav1.Time{Time: time.Now()}, 112 }, 113 } 114 if tc.isComplete { 115 pj.Status.State = prowv1.SuccessState 116 pj.Status.CompletionTime = &metav1.Time{Time: time.Now()} 117 } 118 if tc.hasBuildID { 119 pj.Status.BuildID = "123456789" 120 } 121 if !tc.hasNoPendingTimestamp { 122 pj.Status.PendingTime = &metav1.Time{} 123 } 124 125 kgr := New(fca{}.Config, nil, nil, 1.0, false) 126 shouldReport := kgr.ShouldReport(context.Background(), logrus.NewEntry(logrus.StandardLogger()), pj) 127 if shouldReport != tc.shouldReport { 128 t.Errorf("Expected ShouldReport() to return %v, but got %v", tc.shouldReport, shouldReport) 129 } 130 }) 131 } 132 } 133 134 type testResourceGetter struct { 135 namespace string 136 cluster string 137 pod *v1.Pod 138 events []v1.Event 139 patchData string 140 patchType types.PatchType 141 patchErr error 142 } 143 144 func (rg testResourceGetter) GetPod(_ context.Context, cluster, namespace, name string) (*v1.Pod, error) { 145 if rg.cluster != cluster { 146 return nil, fmt.Errorf("expected cluster %q but got cluster %q", rg.cluster, cluster) 147 } 148 if rg.namespace != namespace { 149 return nil, fmt.Errorf("expected namespace %q but got namespace %q", rg.namespace, namespace) 150 } 151 if rg.pod == nil { 152 return nil, errors.New("no such pod") 153 } 154 if rg.pod.ObjectMeta.Name != name { 155 return nil, fmt.Errorf("expected name %q, but got name %q", rg.pod.ObjectMeta.Name, name) 156 } 157 return rg.pod, nil 158 } 159 160 func (rg testResourceGetter) GetEvents(cluster, namespace string, pod *v1.Pod) ([]v1.Event, error) { 161 if rg.cluster != cluster { 162 return nil, fmt.Errorf("expected cluster %q but got cluster %q", rg.cluster, cluster) 163 } 164 if rg.namespace != namespace { 165 return nil, fmt.Errorf("expected namespace %q but got namespace %q", rg.namespace, namespace) 166 } 167 if pod == nil { 168 return nil, errors.New("expected non-nil pod") 169 } 170 if pod != rg.pod { 171 return nil, errors.New("got the wrong pod") 172 } 173 return rg.events, nil 174 } 175 176 func (rg testResourceGetter) PatchPod(ctx context.Context, cluster, namespace, name string, pt types.PatchType, data []byte) error { 177 if rg.patchErr != nil { 178 return rg.patchErr 179 } 180 if _, err := rg.GetPod(ctx, cluster, namespace, name); err != nil { 181 return err 182 } 183 if rg.patchType != pt { 184 return fmt.Errorf("expected patch type %s, got patchType %s", rg.patchData, pt) 185 } 186 if diff := cmp.Diff(string(data), rg.patchData); diff != "" { 187 return fmt.Errorf("patch differs from expected patch: %s", diff) 188 } 189 190 return nil 191 } 192 193 func TestReportPodInfo(t *testing.T) { 194 tests := []struct { 195 name string 196 pjName string 197 pjComplete bool 198 pjPending bool 199 pjState prowv1.ProwJobState 200 pod *v1.Pod 201 patchErr error 202 events []v1.Event 203 dryRun bool 204 expectReport bool 205 expectErr bool 206 expectedPatch string 207 expectedReconcileResult *reconcile.Result 208 }{ 209 { 210 name: "prowjob picks up pod and events", 211 pjName: "ba123965-4fd4-421f-8509-7590c129ab69", 212 pjComplete: true, 213 pod: &v1.Pod{ 214 ObjectMeta: metav1.ObjectMeta{ 215 Name: "ba123965-4fd4-421f-8509-7590c129ab69", 216 Namespace: "test-pods", 217 Labels: map[string]string{"created-by-prow": "true"}, 218 }, 219 }, 220 events: []v1.Event{ 221 { 222 Type: "Warning", 223 Message: "Some event", 224 }, 225 }, 226 expectReport: true, 227 }, 228 { 229 name: "prowjob with no events reports pod", 230 pjName: "ba123965-4fd4-421f-8509-7590c129ab69", 231 pjComplete: true, 232 pod: &v1.Pod{ 233 ObjectMeta: metav1.ObjectMeta{ 234 Name: "ba123965-4fd4-421f-8509-7590c129ab69", 235 Namespace: "test-pods", 236 Labels: map[string]string{"created-by-prow": "true"}, 237 }, 238 }, 239 expectReport: true, 240 }, 241 { 242 name: "prowjob with no pod reports nothing but does not error", 243 pjName: "ba123965-4fd4-421f-8509-7590c129ab69", 244 pjComplete: true, 245 expectReport: false, 246 }, 247 { 248 name: "nothing is reported in dryrun mode", 249 pjName: "ba123965-4fd4-421f-8509-7590c129ab69", 250 pjComplete: true, 251 dryRun: true, 252 pod: &v1.Pod{ 253 ObjectMeta: metav1.ObjectMeta{ 254 Name: "ba123965-4fd4-421f-8509-7590c129ab69", 255 Namespace: "test-pods", 256 Labels: map[string]string{"created-by-prow": "true"}, 257 }, 258 }, 259 events: []v1.Event{ 260 { 261 Type: "Warning", 262 Message: "Some event", 263 }, 264 }, 265 expectReport: false, 266 }, 267 { 268 name: "Pending incomplete prowjob gets finalizer and is not reported", 269 pjName: "ba123965-4fd4-421f-8509-7590c129ab69", 270 pjPending: true, 271 pjComplete: false, 272 pod: &v1.Pod{ 273 ObjectMeta: metav1.ObjectMeta{ 274 Name: "ba123965-4fd4-421f-8509-7590c129ab69", 275 Namespace: "test-pods", 276 Labels: map[string]string{"created-by-prow": "true"}, 277 }, 278 }, 279 expectReport: false, 280 expectedPatch: `{"metadata":{"finalizers":["prow.x-k8s.io/gcsk8sreporter"]}}`, 281 }, 282 { 283 name: "Finalizer is not added to deleted pod", 284 pjName: "ba123965-4fd4-421f-8509-7590c129ab69", 285 pod: &v1.Pod{ 286 ObjectMeta: metav1.ObjectMeta{ 287 Finalizers: []string{"gcsk8sreporter"}, 288 Name: "ba123965-4fd4-421f-8509-7590c129ab69", 289 Namespace: "test-pods", 290 Labels: map[string]string{"created-by-prow": "true"}, 291 DeletionTimestamp: func() *metav1.Time { t := metav1.Now(); return &t }(), 292 }, 293 }, 294 expectReport: false, 295 expectedPatch: `{"metadata":{"finalizers":null}}`, 296 }, 297 { 298 name: "Pod gets deleted between check and finalizer add request, error is swallowed", 299 pjName: "ba123965-4fd4-421f-8509-7590c129ab69", 300 pod: &v1.Pod{ 301 ObjectMeta: metav1.ObjectMeta{ 302 Name: "ba123965-4fd4-421f-8509-7590c129ab69", 303 Namespace: "test-pods", 304 Labels: map[string]string{"created-by-prow": "true"}, 305 }, 306 }, 307 patchErr: errors.New(`Pod "b2c94437-e0e2-11eb-a92c-0a580a801781" is invalid: metadata.finalizers: Forbidden: no new finalizers can be added if the object is being deleted, found new finalizers []string{"prow.x-k8s.io/gcsk8sreporter"}`), 308 expectReport: false, 309 }, 310 { 311 name: "Finalizer is removed from complete pod", 312 pjName: "ba123965-4fd4-421f-8509-7590c129ab69", 313 pjPending: false, 314 pjComplete: true, 315 pod: &v1.Pod{ 316 ObjectMeta: metav1.ObjectMeta{ 317 Finalizers: []string{"gcsk8sreporter"}, 318 Name: "ba123965-4fd4-421f-8509-7590c129ab69", 319 Namespace: "test-pods", 320 Labels: map[string]string{"created-by-prow": "true"}, 321 }, 322 }, 323 expectReport: true, 324 expectedPatch: `{"metadata":{"finalizers":null}}`, 325 }, 326 { 327 name: "RequeueAfter is returned for incomplete aborted job and nothing happens", 328 pjName: "ba123965-4fd4-421f-8509-7590c129ab69", 329 pjState: prowv1.AbortedState, 330 pjPending: false, 331 pjComplete: false, 332 expectReport: false, 333 expectedReconcileResult: &reconcile.Result{RequeueAfter: 10 * time.Second}, 334 }, 335 { 336 name: "Completed aborted job is reported", 337 pjName: "ba123965-4fd4-421f-8509-7590c129ab69", 338 pjState: prowv1.AbortedState, 339 pjPending: false, 340 pjComplete: true, 341 pod: &v1.Pod{ 342 ObjectMeta: metav1.ObjectMeta{ 343 Finalizers: []string{"gcsk8sreporter"}, 344 Name: "ba123965-4fd4-421f-8509-7590c129ab69", 345 Namespace: "test-pods", 346 Labels: map[string]string{"created-by-prow": "true"}, 347 }, 348 }, 349 expectReport: true, 350 expectedPatch: `{"metadata":{"finalizers":null}}`, 351 }, 352 } 353 354 for _, tc := range tests { 355 t.Run(tc.name, func(t *testing.T) { 356 pj := &prowv1.ProwJob{ 357 ObjectMeta: metav1.ObjectMeta{ 358 Name: tc.pjName, 359 }, 360 Spec: prowv1.ProwJobSpec{ 361 Agent: prowv1.KubernetesAgent, 362 Cluster: "the-build-cluster", 363 Type: prowv1.PeriodicJob, 364 }, 365 Status: prowv1.ProwJobStatus{ 366 State: prowv1.SuccessState, 367 StartTime: metav1.Time{Time: time.Now()}, 368 BuildID: "12345", 369 }, 370 } 371 if tc.pjComplete { 372 pj.Status.CompletionTime = &metav1.Time{Time: time.Now()} 373 } 374 if tc.pjPending { 375 pj.Status.PendingTime = &metav1.Time{} 376 } 377 if tc.pjState != "" { 378 pj.Status.State = tc.pjState 379 } 380 381 fca := fca{c: config.Config{ProwConfig: config.ProwConfig{ 382 PodNamespace: "test-pods", 383 Plank: config.Plank{ 384 DefaultDecorationConfigs: config.DefaultDecorationMapToSliceTesting( 385 map[string]*prowv1.DecorationConfig{"*": { 386 GCSConfiguration: &prowv1.GCSConfiguration{ 387 Bucket: "kubernetes-jenkins", 388 PathPrefix: "some-prefix", 389 PathStrategy: prowv1.PathStrategyLegacy, 390 DefaultOrg: "kubernetes", 391 DefaultRepo: "kubernetes", 392 }, 393 }}), 394 }, 395 }}} 396 397 rg := testResourceGetter{ 398 namespace: "test-pods", 399 cluster: "the-build-cluster", 400 pod: tc.pod, 401 events: tc.events, 402 patchErr: tc.patchErr, 403 patchData: tc.expectedPatch, 404 patchType: types.MergePatchType, 405 } 406 fakeOpener := &fakeopener.FakeOpener{} 407 reporter := New(fca.Config, fakeOpener, rg, 1.0, tc.dryRun) 408 reconcileResult, err := reporter.report(context.Background(), logrus.NewEntry(logrus.StandardLogger()), pj) 409 410 if tc.expectErr { 411 if err == nil { 412 t.Fatal("Expected an error, but didn't get one") 413 } 414 return 415 } 416 if err != nil { 417 t.Fatalf("Unexpected error: %v", err) 418 } 419 420 if diff := cmp.Diff(reconcileResult, tc.expectedReconcileResult); diff != "" { 421 t.Fatalf("reconcileResult differs from expected reconcileResult: %s", diff) 422 } 423 424 var result PodReport 425 var content []byte 426 if fakeOpener.Buffer != nil { 427 content, err = io.ReadAll(fakeOpener.Buffer["gs://kubernetes-jenkins/some-prefix/logs/12345/podinfo.json"]) 428 if err != nil { 429 t.Fatalf("Failed read content: %v", err) 430 } 431 432 if len(content) > 0 { 433 if err = json.Unmarshal(content, &result); err != nil { 434 t.Fatalf("Couldn't unmarshal reported JSON: %v", err) 435 } 436 } 437 } 438 439 if !tc.expectReport { 440 if len(content) > 0 { 441 t.Fatalf("Expected nothing to be written, but something was written: %s", string(content)) 442 } 443 return 444 } 445 446 if !cmp.Equal(result.Pod, tc.pod) { 447 t.Errorf("Got mismatching pods:\n%s", cmp.Diff(tc.pod, result.Pod)) 448 } 449 if !cmp.Equal(result.Events, tc.events) { 450 t.Errorf("Got mismatching events:\n%s", cmp.Diff(tc.events, result.Events)) 451 } 452 }) 453 } 454 }