sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/sidecar/run_test.go (about)

     1  /*
     2  Copyright 2017 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 sidecar
    18  
    19  import (
    20  	"context"
    21  	"errors"
    22  	"fmt"
    23  	"io"
    24  	"os"
    25  	"path"
    26  	"path/filepath"
    27  	"regexp"
    28  	"strconv"
    29  	"strings"
    30  	"sync"
    31  	"testing"
    32  	"time"
    33  
    34  	"github.com/sirupsen/logrus"
    35  	"sigs.k8s.io/prow/pkg/entrypoint"
    36  	"sigs.k8s.io/prow/pkg/gcsupload"
    37  	"sigs.k8s.io/prow/pkg/pod-utils/downwardapi"
    38  	"sigs.k8s.io/prow/pkg/pod-utils/wrapper"
    39  
    40  	"k8s.io/apimachinery/pkg/api/equality"
    41  	"k8s.io/apimachinery/pkg/util/diff"
    42  	"k8s.io/apimachinery/pkg/util/sets"
    43  	prowapi "sigs.k8s.io/prow/pkg/apis/prowjobs/v1"
    44  )
    45  
    46  var re = regexp.MustCompile(`(?m)(Failed to open) .*log\.txt: .*$`)
    47  
    48  func TestWait(t *testing.T) {
    49  	aborted := strconv.Itoa(entrypoint.AbortedErrorCode)
    50  	skip := strconv.Itoa(entrypoint.PreviousErrorCode)
    51  	const (
    52  		pass = "0"
    53  		fail = "1"
    54  	)
    55  	cases := []struct {
    56  		name         string
    57  		markers      []string
    58  		abort        bool
    59  		pass         bool
    60  		accessDenied bool
    61  		missing      bool
    62  		failures     int
    63  	}{
    64  		{
    65  			name:    "pass, not abort when 1 item passes",
    66  			markers: []string{pass},
    67  			pass:    true,
    68  		},
    69  		{
    70  			name:    "pass when all items pass",
    71  			markers: []string{pass, pass, pass},
    72  			pass:    true,
    73  		},
    74  		{
    75  			name:     "fail, not abort when 1 item fails",
    76  			markers:  []string{fail},
    77  			failures: 1,
    78  		},
    79  		{
    80  			name:     "fail when any item fails",
    81  			markers:  []string{pass, fail, pass},
    82  			failures: 1,
    83  		},
    84  		{
    85  			name:     "abort and fail when 1 item aborts",
    86  			markers:  []string{aborted},
    87  			abort:    true,
    88  			failures: 1,
    89  		},
    90  		{
    91  			name:     "abort when any item aborts",
    92  			markers:  []string{pass, aborted, fail},
    93  			abort:    true,
    94  			failures: 2,
    95  		},
    96  		{
    97  			name:     "fail when marker cannot be read",
    98  			markers:  []string{pass, "not-an-exit-code", pass},
    99  			failures: 1,
   100  		},
   101  		{
   102  			name:     "fail when marker does not exist",
   103  			markers:  []string{pass},
   104  			missing:  true,
   105  			failures: 1,
   106  		},
   107  		{
   108  			name:     "count all failures",
   109  			markers:  []string{pass, fail, aborted, skip, fail, pass},
   110  			abort:    true,
   111  			failures: 3,
   112  		},
   113  	}
   114  
   115  	for _, tc := range cases {
   116  		t.Run(tc.name, func(t *testing.T) {
   117  			tmpDir := t.TempDir()
   118  
   119  			var entries []wrapper.Options
   120  
   121  			for i, m := range tc.markers {
   122  				p := path.Join(tmpDir, fmt.Sprintf("marker-%d.txt", i))
   123  				var opt wrapper.Options
   124  				opt.MarkerFile = p
   125  				if err := os.WriteFile(p, []byte(m), 0600); err != nil {
   126  					t.Fatalf("could not create marker %d: %v", i, err)
   127  				}
   128  				entries = append(entries, opt)
   129  			}
   130  
   131  			ctx, cancel := context.WithCancel(context.Background())
   132  			if tc.missing {
   133  				entries = append(entries, wrapper.Options{MarkerFile: "missing-marker.txt"})
   134  				go cancel()
   135  			}
   136  
   137  			pass, abort, failures := wait(ctx, entries)
   138  			cancel()
   139  			if pass != tc.pass {
   140  				t.Errorf("expected pass %t != actual %t", tc.pass, pass)
   141  			}
   142  			if abort != tc.abort {
   143  				t.Errorf("expected abort %t != actual %t", tc.abort, abort)
   144  			}
   145  			if failures != tc.failures {
   146  				t.Errorf("expected failures %d != actual %d", tc.failures, failures)
   147  			}
   148  		})
   149  	}
   150  }
   151  
   152  func TestWaitParallelContainers(t *testing.T) {
   153  	aborted := strconv.Itoa(entrypoint.AbortedErrorCode)
   154  	skip := strconv.Itoa(entrypoint.PreviousErrorCode)
   155  	const (
   156  		pass                 = "0"
   157  		fail                 = "1"
   158  		missingMarkerTimeout = time.Second
   159  	)
   160  	cases := []struct {
   161  		name         string
   162  		markers      []string
   163  		abort        bool
   164  		pass         bool
   165  		accessDenied bool
   166  		missing      bool
   167  		failures     int
   168  	}{
   169  		{
   170  			name:    "pass, not abort when 1 item passes",
   171  			markers: []string{pass},
   172  			pass:    true,
   173  		},
   174  		{
   175  			name:    "pass when all items pass",
   176  			markers: []string{pass, pass, pass},
   177  			pass:    true,
   178  		},
   179  		{
   180  			name:     "fail, not abort when 1 item fails",
   181  			markers:  []string{fail},
   182  			failures: 1,
   183  		},
   184  		{
   185  			name:     "fail when any item fails",
   186  			markers:  []string{pass, fail, pass},
   187  			failures: 1,
   188  		},
   189  		{
   190  			name:     "abort and fail when 1 item aborts",
   191  			markers:  []string{aborted},
   192  			abort:    true,
   193  			failures: 1,
   194  		},
   195  		{
   196  			name:     "abort when any item aborts",
   197  			markers:  []string{pass, aborted, fail},
   198  			abort:    true,
   199  			failures: 2,
   200  		},
   201  		{
   202  			name:     "fail when marker does not exist",
   203  			markers:  []string{pass},
   204  			missing:  true,
   205  			failures: 1,
   206  		},
   207  		{
   208  			name:     "count all failures",
   209  			markers:  []string{pass, fail, aborted, skip, fail, pass},
   210  			abort:    true,
   211  			failures: 3,
   212  		},
   213  	}
   214  
   215  	for _, tc := range cases {
   216  		t.Run(tc.name, func(t *testing.T) {
   217  			tmpDir := t.TempDir()
   218  
   219  			var entries []wrapper.Options
   220  
   221  			for i := range tc.markers {
   222  				p := path.Join(tmpDir, fmt.Sprintf("marker-%d.txt", i))
   223  				var opt wrapper.Options
   224  				opt.MarkerFile = p
   225  				entries = append(entries, opt)
   226  			}
   227  
   228  			if tc.missing {
   229  				missingPath := path.Join(tmpDir, "missing-marker.txt")
   230  				entries = append(entries, wrapper.Options{MarkerFile: missingPath})
   231  			}
   232  
   233  			ctx, cancel := context.WithCancel(context.Background())
   234  
   235  			type WaitResult struct {
   236  				pass     bool
   237  				abort    bool
   238  				failures int
   239  			}
   240  
   241  			waitResultsCh := make(chan WaitResult)
   242  
   243  			go func() {
   244  				pass, abort, failures := wait(ctx, entries)
   245  				waitResultsCh <- WaitResult{pass, abort, failures}
   246  			}()
   247  
   248  			errCh := make(chan error, len(tc.markers))
   249  			for i, m := range tc.markers {
   250  
   251  				options := entries[i]
   252  
   253  				entrypointOptions := entrypoint.Options{
   254  					Options: &options,
   255  				}
   256  				marker, err := strconv.Atoi(m)
   257  				if err != nil {
   258  					errCh <- fmt.Errorf("invalid exit code: %w", err)
   259  				}
   260  				go func() {
   261  					errCh <- entrypointOptions.Mark(marker)
   262  				}()
   263  
   264  			}
   265  
   266  			if tc.missing {
   267  				go func() {
   268  					time.Sleep(missingMarkerTimeout)
   269  					cancel()
   270  					errCh <- nil
   271  				}()
   272  			}
   273  
   274  			for range tc.markers {
   275  				if err := <-errCh; err != nil {
   276  					t.Fatalf("could not create marker: %v", err)
   277  				}
   278  			}
   279  
   280  			waitRes := <-waitResultsCh
   281  
   282  			cancel()
   283  			if waitRes.pass != tc.pass {
   284  				t.Errorf("expected pass %t != actual %t", tc.pass, waitRes.pass)
   285  			}
   286  			if waitRes.abort != tc.abort {
   287  				t.Errorf("expected abort %t != actual %t", tc.abort, waitRes.abort)
   288  			}
   289  			if waitRes.failures != tc.failures {
   290  				t.Errorf("expected failures %d != actual %d", tc.failures, waitRes.failures)
   291  			}
   292  		})
   293  	}
   294  }
   295  
   296  func TestCombineMetadata(t *testing.T) {
   297  	cases := []struct {
   298  		name     string
   299  		pieces   []string
   300  		expected map[string]interface{}
   301  	}{
   302  		{
   303  			name:   "no problem when metadata file is not there",
   304  			pieces: []string{"missing"},
   305  		},
   306  		{
   307  			name:   "simple metadata",
   308  			pieces: []string{`{"hello": "world"}`},
   309  			expected: map[string]interface{}{
   310  				"hello": "world",
   311  			},
   312  		},
   313  		{
   314  			name: "merge pieces",
   315  			pieces: []string{
   316  				`{"hello": "hello", "world": "world", "first": 1}`,
   317  				`{"hello": "hola", "world": "world", "second": 2}`,
   318  			},
   319  			expected: map[string]interface{}{
   320  				"hello":  "hola",
   321  				"world":  "world",
   322  				"first":  1.0,
   323  				"second": 2.0,
   324  			},
   325  		},
   326  		{
   327  			name: "errors go into sidecar-errors",
   328  			pieces: []string{
   329  				`{"hello": "there"}`,
   330  				"missing",
   331  				"read-error",
   332  				"json-error", // this is invalid json
   333  				`{"world": "thanks"}`,
   334  			},
   335  			expected: map[string]interface{}{
   336  				"hello": "there",
   337  				"world": "thanks",
   338  				errorKey: map[string]error{
   339  					name(2): errors.New("read"),
   340  					name(3): errors.New("json"),
   341  				},
   342  			},
   343  		},
   344  	}
   345  
   346  	for _, tc := range cases {
   347  		t.Run(tc.name, func(t *testing.T) {
   348  			tmpDir := t.TempDir()
   349  			var entries []wrapper.Options
   350  
   351  			for i, m := range tc.pieces {
   352  				p := path.Join(tmpDir, fmt.Sprintf("metadata-%d.txt", i))
   353  				var opt wrapper.Options
   354  				opt.MetadataFile = p
   355  				entries = append(entries, opt)
   356  				if m == "missing" {
   357  					continue
   358  				} else if m == "read-error" {
   359  					if err := os.Mkdir(p, 0700); err != nil {
   360  						t.Fatalf("could not create %s: %v", p, err)
   361  					}
   362  					continue
   363  				}
   364  				// not-json is invalid json
   365  				if err := os.WriteFile(p, []byte(m), 0600); err != nil {
   366  					t.Fatalf("could not create metadata %d: %v", i, err)
   367  				}
   368  			}
   369  
   370  			actual := combineMetadata(entries)
   371  			expectedErrors, _ := tc.expected[errorKey].(map[string]error)
   372  			actualErrors, _ := actual[errorKey].(map[string]error)
   373  			delete(tc.expected, errorKey)
   374  			delete(actual, errorKey)
   375  			if !equality.Semantic.DeepEqual(tc.expected, actual) {
   376  				t.Errorf("maps do not match:\n%s", diff.ObjectReflectDiff(tc.expected, actual))
   377  			}
   378  
   379  			if !equality.Semantic.DeepEqual(sets.KeySet[string](expectedErrors), sets.KeySet[string](actualErrors)) { // ignore the error values
   380  				t.Errorf("errors do not match:\n%s", diff.ObjectReflectDiff(expectedErrors, actualErrors))
   381  			}
   382  		})
   383  	}
   384  }
   385  
   386  func name(idx int) string {
   387  	return nameEntry(idx, wrapper.Options{})
   388  }
   389  
   390  func TestLogReaders(t *testing.T) {
   391  	cases := []struct {
   392  		name           string
   393  		containerNames []string
   394  		processLogs    map[string]string
   395  		expected       map[string]string
   396  	}{
   397  		{
   398  			name: "works with 1 container",
   399  			containerNames: []string{
   400  				"test",
   401  			},
   402  			processLogs: map[string]string{
   403  				"process-log.txt": "hello world",
   404  			},
   405  			expected: map[string]string{
   406  				"build-log.txt": "hello world",
   407  			},
   408  		},
   409  		{
   410  			name: "works with 1 container with no name",
   411  			containerNames: []string{
   412  				"",
   413  			},
   414  			processLogs: map[string]string{
   415  				"process-log.txt": "hello world",
   416  			},
   417  			expected: map[string]string{
   418  				"build-log.txt": "hello world",
   419  			},
   420  		},
   421  		{
   422  			name: "multiple logs works",
   423  			containerNames: []string{
   424  				"test1",
   425  				"test2",
   426  			},
   427  			processLogs: map[string]string{
   428  				"test1-log.txt": "hello",
   429  				"test2-log.txt": "world",
   430  			},
   431  			expected: map[string]string{
   432  				"test1-build-log.txt": "hello",
   433  				"test2-build-log.txt": "world",
   434  			},
   435  		},
   436  		{
   437  			name: "note when a part has a problem",
   438  			containerNames: []string{
   439  				"test1",
   440  				"test2",
   441  				"test3",
   442  			},
   443  			processLogs: map[string]string{
   444  				"test1-log.txt": "hello",
   445  				"test2-log.txt": "missing",
   446  				"test3-log.txt": "world",
   447  			},
   448  			expected: map[string]string{
   449  				"test1-build-log.txt": "hello",
   450  				"test2-build-log.txt": "Failed to open test2-log.txt: whatever\n",
   451  				"test3-build-log.txt": "world",
   452  			},
   453  		},
   454  	}
   455  
   456  	for _, tc := range cases {
   457  		t.Run(tc.name, func(t *testing.T) {
   458  			tmpDir := t.TempDir()
   459  
   460  			for name, log := range tc.processLogs {
   461  				p := path.Join(tmpDir, name)
   462  				if log == "missing" {
   463  					continue
   464  				}
   465  				if err := os.WriteFile(p, []byte(log), 0600); err != nil {
   466  					t.Fatalf("could not create log %s: %v", name, err)
   467  				}
   468  			}
   469  
   470  			var entries []wrapper.Options
   471  
   472  			for _, containerName := range tc.containerNames {
   473  				log := "process-log.txt"
   474  				if len(tc.containerNames) > 1 {
   475  					log = fmt.Sprintf("%s-log.txt", containerName)
   476  				}
   477  				p := path.Join(tmpDir, log)
   478  				var opt wrapper.Options
   479  				opt.ProcessLog = p
   480  				opt.ContainerName = containerName
   481  				entries = append(entries, opt)
   482  			}
   483  
   484  			readers := logReadersFuncs(entries)
   485  			const repl = "$1 <SNIP>"
   486  			actual := make(map[string]string)
   487  			for name, newReader := range readers {
   488  				r, err := newReader()
   489  				if err != nil {
   490  					t.Fatalf("failed to make reader: %v", err)
   491  				}
   492  				buf, err := io.ReadAll(r)
   493  				if err != nil {
   494  					t.Fatalf("failed to read all: %v", err)
   495  				}
   496  				actual[name] = re.ReplaceAllString(string(buf), repl)
   497  				if err := r.Close(); err != nil {
   498  					t.Fatalf("failed to close reader: %v", err)
   499  				}
   500  			}
   501  
   502  			for name, log := range tc.expected {
   503  				tc.expected[name] = re.ReplaceAllString(log, repl)
   504  			}
   505  
   506  			if !equality.Semantic.DeepEqual(tc.expected, actual) {
   507  				t.Errorf("maps do not match:\n%s", diff.ObjectReflectDiff(tc.expected, actual))
   508  			}
   509  		})
   510  	}
   511  
   512  }
   513  
   514  func TestSideCarLogsUpload(t *testing.T) {
   515  	logFile, err := LogSetup()
   516  	if err != nil {
   517  		t.Fatalf("Unable to set up log file")
   518  	}
   519  	defer os.Remove(logFile.Name())
   520  	testString := "Testing...Hello world!"
   521  	logrus.Info(testString)
   522  	var once sync.Once
   523  
   524  	localOutputDir := t.TempDir()
   525  
   526  	options := Options{
   527  		GcsOptions: &gcsupload.Options{
   528  			GCSConfiguration: &prowapi.GCSConfiguration{
   529  				PathStrategy:   prowapi.PathStrategyExplicit,
   530  				Bucket:         "bucket",
   531  				LocalOutputDir: localOutputDir,
   532  			},
   533  		},
   534  	}
   535  
   536  	spec := &downwardapi.JobSpec{
   537  		Job:  "job",
   538  		Type: prowapi.PostsubmitJob,
   539  		Refs: &prowapi.Refs{
   540  			Org:  "org",
   541  			Repo: "repo",
   542  			Pulls: []prowapi.Pull{
   543  				{
   544  					Number: 1,
   545  				},
   546  			},
   547  		},
   548  		BuildID: "build",
   549  	}
   550  
   551  	entries := options.entries()
   552  	metadata := combineMetadata(entries)
   553  	buildLogs := logReadersFuncs(entries)
   554  
   555  	options.doUpload(context.Background(), spec, true, false, metadata, buildLogs, logFile, &once)
   556  
   557  	files, err := os.ReadDir(localOutputDir)
   558  	if err != nil {
   559  		t.Errorf("Unable to access files in directory: %v", err)
   560  	}
   561  	if len(files) == 0 {
   562  		t.Fatal("Log file was not uploaded")
   563  	}
   564  
   565  	var s []byte
   566  	s, err = os.ReadFile(filepath.Join(localOutputDir, LogFileName))
   567  	if err != nil {
   568  		t.Fatalf("Unable to read log file: %v", err)
   569  	}
   570  
   571  	f := string(s)
   572  	if !strings.Contains(f, testString) {
   573  		t.Fatal("Log file not correctly capturing logs")
   574  	}
   575  
   576  }