sigs.k8s.io/controller-runtime@v0.18.2/pkg/internal/testing/process/process_test.go (about) 1 /* 2 Copyright 2021 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 process_test 18 19 import ( 20 "bytes" 21 "net" 22 "net/http" 23 "net/url" 24 "os" 25 "strconv" 26 "time" 27 28 . "github.com/onsi/ginkgo/v2" 29 . "github.com/onsi/gomega" 30 "github.com/onsi/gomega/ghttp" 31 "sigs.k8s.io/controller-runtime/pkg/internal/testing/addr" 32 . "sigs.k8s.io/controller-runtime/pkg/internal/testing/process" 33 ) 34 35 const ( 36 healthURLPath = "/healthz" 37 ) 38 39 var _ = Describe("Start method", func() { 40 var ( 41 processState *State 42 server *ghttp.Server 43 ) 44 BeforeEach(func() { 45 server = ghttp.NewServer() 46 47 processState = &State{ 48 Path: "bash", 49 Args: simpleBashScript, 50 HealthCheck: HealthCheck{ 51 URL: getServerURL(server), 52 }, 53 } 54 processState.Path = "bash" 55 processState.Args = simpleBashScript 56 57 }) 58 AfterEach(func() { 59 server.Close() 60 }) 61 62 Context("when process takes too long to start", func() { 63 BeforeEach(func() { 64 server.RouteToHandler("GET", healthURLPath, func(resp http.ResponseWriter, _ *http.Request) { 65 time.Sleep(250 * time.Millisecond) 66 resp.WriteHeader(http.StatusOK) 67 }) 68 }) 69 It("returns a timeout error", func() { 70 processState.StartTimeout = 200 * time.Millisecond 71 72 err := processState.Start(nil, nil) 73 Expect(err).To(MatchError(ContainSubstring("timeout"))) 74 75 Eventually(func() bool { done, _ := processState.Exited(); return done }).Should(BeTrue()) 76 }) 77 }) 78 79 Context("when the healthcheck returns ok", func() { 80 BeforeEach(func() { 81 82 server.RouteToHandler("GET", healthURLPath, ghttp.RespondWith(http.StatusOK, "")) 83 }) 84 85 It("can start a process", func() { 86 processState.StartTimeout = 10 * time.Second 87 88 err := processState.Start(nil, nil) 89 Expect(err).NotTo(HaveOccurred()) 90 91 Consistently(processState.Exited).Should(BeFalse()) 92 }) 93 94 It("hits the endpoint, and successfully starts", func() { 95 processState.StartTimeout = 100 * time.Millisecond 96 97 err := processState.Start(nil, nil) 98 Expect(err).NotTo(HaveOccurred()) 99 Expect(server.ReceivedRequests()).To(HaveLen(1)) 100 Consistently(processState.Exited).Should(BeFalse()) 101 }) 102 103 Context("when the command cannot be started", func() { 104 var err error 105 106 BeforeEach(func() { 107 processState = &State{} 108 processState.Path = "/nonexistent" 109 110 err = processState.Start(nil, nil) 111 }) 112 113 It("propagates the error", func() { 114 Expect(os.IsNotExist(err)).To(BeTrue()) 115 }) 116 117 Context("but Stop() is called on it", func() { 118 It("does not panic", func() { 119 stoppingFailedProcess := func() { 120 Expect(processState.Stop()).To(Succeed()) 121 } 122 123 Expect(stoppingFailedProcess).NotTo(Panic()) 124 }) 125 }) 126 }) 127 128 Context("when IO is configured", func() { 129 It("can inspect stdout & stderr", func() { 130 stdout := &bytes.Buffer{} 131 stderr := &bytes.Buffer{} 132 133 processState.Args = []string{ 134 "-c", 135 ` 136 echo 'this is stderr' >&2 137 echo 'that is stdout' 138 echo 'i started' >&2 139 `, 140 } 141 processState.StartTimeout = 5 * time.Second 142 143 Expect(processState.Start(stdout, stderr)).To(Succeed()) 144 Eventually(processState.Exited).Should(BeTrue()) 145 146 Expect(stdout.String()).To(Equal("that is stdout\n")) 147 Expect(stderr.String()).To(Equal("this is stderr\ni started\n")) 148 }) 149 }) 150 }) 151 152 Context("when the healthcheck always returns failure", func() { 153 BeforeEach(func() { 154 server.RouteToHandler("GET", healthURLPath, ghttp.RespondWith(http.StatusInternalServerError, "")) 155 }) 156 It("returns a timeout error and stops health API checker", func() { 157 processState.HealthCheck.URL = getServerURL(server) 158 processState.HealthCheck.Path = healthURLPath 159 processState.StartTimeout = 500 * time.Millisecond 160 161 err := processState.Start(nil, nil) 162 Expect(err).To(MatchError(ContainSubstring("timeout"))) 163 164 nrReceivedRequests := len(server.ReceivedRequests()) 165 Expect(nrReceivedRequests).To(Equal(5)) 166 time.Sleep(200 * time.Millisecond) 167 Expect(nrReceivedRequests).To(Equal(5)) 168 }) 169 }) 170 171 Context("when the healthcheck isn't even listening", func() { 172 BeforeEach(func() { 173 server.Close() 174 }) 175 176 It("returns a timeout error", func() { 177 processState.HealthCheck.Path = healthURLPath 178 processState.StartTimeout = 500 * time.Millisecond 179 180 port, host, err := addr.Suggest("") 181 Expect(err).NotTo(HaveOccurred()) 182 183 processState.HealthCheck.URL = url.URL{ 184 Scheme: "http", 185 Host: net.JoinHostPort(host, strconv.Itoa(port)), 186 } 187 188 err = processState.Start(nil, nil) 189 Expect(err).To(MatchError(ContainSubstring("timeout"))) 190 }) 191 }) 192 193 Context("when the healthcheck fails initially but succeeds eventually", func() { 194 BeforeEach(func() { 195 server.AppendHandlers( 196 ghttp.RespondWith(http.StatusInternalServerError, ""), 197 ghttp.RespondWith(http.StatusInternalServerError, ""), 198 ghttp.RespondWith(http.StatusInternalServerError, ""), 199 ghttp.RespondWith(http.StatusOK, ""), 200 ) 201 }) 202 203 It("hits the endpoint repeatedly, and successfully starts", func() { 204 processState.HealthCheck.URL = getServerURL(server) 205 processState.HealthCheck.Path = healthURLPath 206 processState.StartTimeout = 20 * time.Second 207 208 err := processState.Start(nil, nil) 209 Expect(err).NotTo(HaveOccurred()) 210 Expect(server.ReceivedRequests()).To(HaveLen(4)) 211 Consistently(processState.Exited).Should(BeFalse()) 212 }) 213 214 Context("when the polling interval is not configured", func() { 215 It("uses the default interval for polling", func() { 216 processState.HealthCheck.URL = getServerURL(server) 217 processState.HealthCheck.Path = "/helathz" 218 processState.StartTimeout = 300 * time.Millisecond 219 220 Expect(processState.Start(nil, nil)).To(MatchError(ContainSubstring("timeout"))) 221 Expect(server.ReceivedRequests()).To(HaveLen(3)) 222 }) 223 }) 224 225 Context("when the polling interval is configured", func() { 226 BeforeEach(func() { 227 processState.HealthCheck.URL = getServerURL(server) 228 processState.HealthCheck.Path = healthURLPath 229 processState.HealthCheck.PollInterval = time.Millisecond * 150 230 }) 231 232 It("hits the endpoint in the configured interval", func() { 233 processState.StartTimeout = 3 * processState.HealthCheck.PollInterval 234 235 Expect(processState.Start(nil, nil)).To(MatchError(ContainSubstring("timeout"))) 236 Expect(server.ReceivedRequests()).To(HaveLen(3)) 237 }) 238 }) 239 }) 240 }) 241 242 var _ = Describe("Stop method", func() { 243 var ( 244 server *ghttp.Server 245 processState *State 246 ) 247 BeforeEach(func() { 248 server = ghttp.NewServer() 249 server.RouteToHandler("GET", healthURLPath, ghttp.RespondWith(http.StatusOK, "")) 250 processState = &State{ 251 Path: "bash", 252 Args: simpleBashScript, 253 HealthCheck: HealthCheck{ 254 URL: getServerURL(server), 255 }, 256 } 257 processState.StartTimeout = 10 * time.Second 258 }) 259 260 AfterEach(func() { 261 server.Close() 262 }) 263 Context("when Stop() is called", func() { 264 BeforeEach(func() { 265 Expect(processState.Start(nil, nil)).To(Succeed()) 266 processState.StopTimeout = 10 * time.Second 267 }) 268 269 It("stops the process", func() { 270 Expect(processState.Stop()).To(Succeed()) 271 }) 272 273 Context("multiple times", func() { 274 It("does not error or panic on consecutive calls", func() { 275 stoppingTheProcess := func() { 276 Expect(processState.Stop()).To(Succeed()) 277 } 278 Expect(stoppingTheProcess).NotTo(Panic()) 279 Expect(stoppingTheProcess).NotTo(Panic()) 280 Expect(stoppingTheProcess).NotTo(Panic()) 281 }) 282 }) 283 }) 284 285 Context("when the command cannot be stopped", func() { 286 It("returns a timeout error", func() { 287 Expect(processState.Start(nil, nil)).To(Succeed()) 288 processState.StopTimeout = 1 * time.Nanosecond // much shorter than the sleep in the script 289 290 Expect(processState.Stop()).To(MatchError(ContainSubstring("timeout"))) 291 }) 292 }) 293 294 Context("when the directory needs to be cleaned up", func() { 295 It("removes the directory", func() { 296 var err error 297 298 Expect(processState.Start(nil, nil)).To(Succeed()) 299 processState.Dir, err = os.MkdirTemp("", "k8s_test_framework_") 300 Expect(err).NotTo(HaveOccurred()) 301 processState.DirNeedsCleaning = true 302 processState.StopTimeout = 400 * time.Millisecond 303 304 Expect(processState.Stop()).To(Succeed()) 305 Expect(processState.Dir).NotTo(BeAnExistingFile()) 306 }) 307 }) 308 }) 309 310 var _ = Describe("Init", func() { 311 Context("when all inputs are provided", func() { 312 It("passes them through", func() { 313 ps := &State{ 314 Dir: "/some/dir", 315 Path: "/some/path/to/some/bin", 316 StartTimeout: 20 * time.Hour, 317 StopTimeout: 65537 * time.Millisecond, 318 } 319 320 Expect(ps.Init("some name")).To(Succeed()) 321 322 Expect(ps.Dir).To(Equal("/some/dir")) 323 Expect(ps.DirNeedsCleaning).To(BeFalse()) 324 Expect(ps.Path).To(Equal("/some/path/to/some/bin")) 325 Expect(ps.StartTimeout).To(Equal(20 * time.Hour)) 326 Expect(ps.StopTimeout).To(Equal(65537 * time.Millisecond)) 327 }) 328 }) 329 330 Context("when inputs are empty", func() { 331 It("ps them", func() { 332 ps := &State{} 333 Expect(ps.Init("some name")).To(Succeed()) 334 335 Expect(ps.Dir).To(BeADirectory()) 336 Expect(os.RemoveAll(ps.Dir)).To(Succeed()) 337 Expect(ps.DirNeedsCleaning).To(BeTrue()) 338 339 Expect(ps.Path).NotTo(BeEmpty()) 340 341 Expect(ps.StartTimeout).NotTo(BeZero()) 342 Expect(ps.StopTimeout).NotTo(BeZero()) 343 }) 344 }) 345 346 Context("when neither name nor path are provided", func() { 347 It("returns an error", func() { 348 ps := &State{} 349 Expect(ps.Init("")).To(MatchError("must have at least one of name or path")) 350 }) 351 }) 352 }) 353 354 var simpleBashScript = []string{ 355 "-c", "tail -f /dev/null", 356 } 357 358 func getServerURL(server *ghttp.Server) url.URL { 359 url, err := url.Parse(server.URL()) 360 Expect(err).NotTo(HaveOccurred()) 361 url.Path = healthURLPath 362 return *url 363 }