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  }