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 }