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

     1  /*
     2  Copyright 2018 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 entrypoint
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"os"
    23  	"path"
    24  	"strconv"
    25  	"syscall"
    26  	"testing"
    27  	"time"
    28  
    29  	"github.com/sirupsen/logrus"
    30  	"sigs.k8s.io/prow/pkg/pod-utils/wrapper"
    31  )
    32  
    33  func TestOptions_Run(t *testing.T) {
    34  	var testCases = []struct {
    35  		name           string
    36  		args           []string
    37  		alwaysZero     bool
    38  		interrupt      bool
    39  		propagate      bool
    40  		invalidMarker  bool
    41  		previousMarker string
    42  		timeout        time.Duration
    43  		gracePeriod    time.Duration
    44  		expectedLog    string
    45  		expectedMarker string
    46  		expectedCode   int
    47  	}{
    48  		{
    49  			name:           "successful command",
    50  			args:           []string{"sh", "-c", "exit 0"},
    51  			expectedLog:    "",
    52  			expectedMarker: "0",
    53  			expectedCode:   0,
    54  		},
    55  		{
    56  			name:           "successful command with output",
    57  			args:           []string{"echo", "test"},
    58  			expectedLog:    "test\n",
    59  			expectedMarker: "0",
    60  			expectedCode:   0,
    61  		},
    62  		{
    63  			name:           "unsuccessful command",
    64  			args:           []string{"sh", "-c", "exit 12"},
    65  			expectedLog:    "",
    66  			expectedMarker: "12",
    67  			expectedCode:   12,
    68  		},
    69  		{
    70  			name:           "unsuccessful command with output",
    71  			args:           []string{"sh", "-c", "echo test && exit 12"},
    72  			expectedLog:    "test\n",
    73  			expectedMarker: "12",
    74  			expectedCode:   12,
    75  		},
    76  		{
    77  			name:           "command times out",
    78  			args:           []string{"sleep", "10"},
    79  			timeout:        1 * time.Second,
    80  			gracePeriod:    1 * time.Second,
    81  			expectedLog:    "level=error msg=\"Process did not finish before 1s timeout\"\nlevel=error msg=\"Process gracefully exited before 1s grace period\"\n",
    82  			expectedMarker: strconv.Itoa(InternalErrorCode),
    83  			expectedCode:   InternalErrorCode,
    84  		},
    85  		{
    86  			name:           "command times out and ignores interrupt",
    87  			args:           []string{"bash", "-c", "trap 'sleep 10' EXIT; sleep 10"},
    88  			timeout:        1 * time.Second,
    89  			gracePeriod:    1 * time.Second,
    90  			expectedLog:    "level=error msg=\"Process did not finish before 1s timeout\"\nlevel=error msg=\"Process did not exit before 1s grace period\"\n",
    91  			expectedMarker: strconv.Itoa(InternalErrorCode),
    92  			expectedCode:   InternalErrorCode,
    93  		},
    94  		{
    95  			// Ensure that environment variables get passed through
    96  			name:           "$PATH is set",
    97  			args:           []string{"sh", "-c", "echo $PATH"},
    98  			expectedLog:    os.Getenv("PATH") + "\n",
    99  			expectedMarker: "0",
   100  			expectedCode:   0,
   101  		},
   102  		{
   103  			name:           "failures return 0 when AlwaysZero is set",
   104  			alwaysZero:     true,
   105  			args:           []string{"sh", "-c", "exit 7"},
   106  			expectedMarker: "7",
   107  			expectedCode:   0,
   108  		},
   109  		{
   110  			name:           "return non-zero when writing marker fails even when AlwaysZero is set",
   111  			alwaysZero:     true,
   112  			timeout:        1 * time.Second,
   113  			gracePeriod:    1 * time.Second,
   114  			args:           []string{"echo", "test"},
   115  			invalidMarker:  true,
   116  			expectedLog:    "test\n",
   117  			expectedMarker: strconv.Itoa(InternalErrorCode),
   118  			expectedCode:   InternalErrorCode,
   119  		},
   120  		{
   121  			name:           "return PreviousErrorCode without running anything if previous marker failed",
   122  			previousMarker: "9",
   123  			args:           []string{"echo", "test"},
   124  			expectedLog:    "level=info msg=\"Skipping as previous step exited 9\"\n",
   125  			expectedCode:   PreviousErrorCode,
   126  			expectedMarker: strconv.Itoa(PreviousErrorCode),
   127  		},
   128  		{
   129  			name:           "run passing command as normal if previous marker passed",
   130  			previousMarker: "0",
   131  			args:           []string{"sh", "-c", "exit 0"},
   132  			expectedMarker: "0",
   133  			expectedCode:   0,
   134  		},
   135  
   136  		{
   137  			name:      "interrupt, propagate child error",
   138  			interrupt: true,
   139  			propagate: true,
   140  			args: []string{"bash", "-c", `function cleanup() {
   141  CHILDREN=$(jobs -p)
   142  if test -n "${CHILDREN}"
   143  then
   144  kill ${CHILDREN} && wait
   145  fi
   146  exit 3
   147  }
   148  trap cleanup SIGINT SIGTERM EXIT
   149  echo process started
   150  sleep infinity &
   151  wait`},
   152  			expectedLog:    "process started\nlevel=error msg=\"Entrypoint received interrupt: terminated\"\nlevel=error msg=\"Process gracefully exited before 15s grace period\"\n",
   153  			expectedMarker: "3",
   154  			expectedCode:   3,
   155  		},
   156  		{
   157  			name:      "interrupt, do not propagate child error",
   158  			interrupt: true,
   159  			args: []string{"bash", "-c", `function cleanup() {
   160  CHILDREN=$(jobs -p)
   161  if test -n "${CHILDREN}"
   162  then
   163  kill ${CHILDREN} && wait
   164  fi
   165  exit 3
   166  }
   167  trap cleanup SIGINT SIGTERM EXIT
   168  echo process started
   169  sleep infinity &
   170  wait`},
   171  			expectedLog:    "process started\nlevel=error msg=\"Entrypoint received interrupt: terminated\"\nlevel=error msg=\"Process gracefully exited before 15s grace period\"\n",
   172  			expectedMarker: "130",
   173  			expectedCode:   130,
   174  		},
   175  		{
   176  			name:           "run failing command as normal if previous marker passed",
   177  			previousMarker: "0",
   178  			args:           []string{"sh", "-c", "exit 4"},
   179  			expectedMarker: "4",
   180  			expectedCode:   4,
   181  		},
   182  		{
   183  			name:           "start error is written to log",
   184  			args:           []string{"./this-command-does-not-exist"},
   185  			expectedLog:    "could not start the process: fork/exec ./this-command-does-not-exist: no such file or directory",
   186  			expectedMarker: "127",
   187  			expectedCode:   InternalErrorCode,
   188  		},
   189  	}
   190  
   191  	// we write logs to the process log if wrapping fails
   192  	// and cannot write timestamps or we can't match text
   193  	logrus.SetFormatter(&logrus.TextFormatter{DisableTimestamp: true})
   194  
   195  	for _, testCase := range testCases {
   196  		t.Run(testCase.name, func(t *testing.T) {
   197  			tmpDir := t.TempDir()
   198  			interrupt := make(chan os.Signal, 1)
   199  
   200  			options := Options{
   201  				AlwaysZero:         testCase.alwaysZero,
   202  				PropagateErrorCode: testCase.propagate,
   203  				Timeout:            testCase.timeout,
   204  				GracePeriod:        testCase.gracePeriod,
   205  				Options: &wrapper.Options{
   206  					Args:       testCase.args,
   207  					ProcessLog: path.Join(tmpDir, "process-log.txt"),
   208  					MarkerFile: path.Join(tmpDir, "marker-file.txt"),
   209  				},
   210  			}
   211  
   212  			if testCase.previousMarker != "" {
   213  				p := path.Join(tmpDir, "previous-marker.txt")
   214  				options.PreviousMarker = p
   215  				if err := os.WriteFile(p, []byte(testCase.previousMarker), 0600); err != nil {
   216  					t.Fatalf("could not create previous marker: %v", err)
   217  				}
   218  			}
   219  
   220  			if testCase.invalidMarker {
   221  				options.MarkerFile = "/this/had/better/not/be/a/real/file!@!#$%#$^#%&*&&*()*"
   222  			}
   223  
   224  			if testCase.interrupt {
   225  				go func() {
   226  					ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
   227  					defer cancel()
   228  					// sync with ExecuteProcess func to ensure that process has already started
   229  					if err := waitForFileToBeWritten(ctx, options.ProcessLog); err != nil {
   230  						t.Errorf("failed to wait for file: %v", err)
   231  					}
   232  					time.Sleep(200 * time.Millisecond)
   233  					interrupt <- syscall.SIGTERM
   234  				}()
   235  			}
   236  
   237  			if code := options.internalRun(interrupt); code != testCase.expectedCode {
   238  				t.Errorf("%s: expected exit code %d != actual %d", testCase.name, testCase.expectedCode, code)
   239  			}
   240  
   241  			compareFileContents(testCase.name, options.ProcessLog, testCase.expectedLog, t)
   242  			if !testCase.invalidMarker {
   243  				compareFileContents(testCase.name, options.MarkerFile, testCase.expectedMarker, t)
   244  			}
   245  		})
   246  	}
   247  }
   248  
   249  func compareFileContents(name, file, expected string, t *testing.T) {
   250  	data, err := os.ReadFile(file)
   251  	if err != nil {
   252  		t.Fatalf("%s: could not read file: %v", name, err)
   253  	}
   254  	if string(data) != expected {
   255  		t.Errorf("%s: expected contents: %q, got %q", name, expected, data)
   256  	}
   257  }
   258  
   259  func waitForFileToBeWritten(ctx context.Context, file string) error {
   260  	ticker := time.NewTicker(100 * time.Millisecond)
   261  	defer ticker.Stop()
   262  	for {
   263  		select {
   264  		case <-ticker.C:
   265  			fileInfo, _ := os.Stat(file)
   266  			if fileInfo.Size() != 0 {
   267  				return nil
   268  			}
   269  		case <-ctx.Done():
   270  			return fmt.Errorf("cancelled while waiting for file %s to exist", file)
   271  		}
   272  	}
   273  }