...

Source file src/sigs.k8s.io/controller-runtime/pkg/internal/testing/process/process_test.go

Documentation: sigs.k8s.io/controller-runtime/pkg/internal/testing/process

     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  }
   364  

View as plain text