hs-test: retry command on test setup failure
[vpp.git] / extras / hs-test / container.go
1 package main
2
3 import (
4         "fmt"
5         "os"
6         "os/exec"
7         "strings"
8         "text/template"
9         "time"
10
11         "github.com/edwarnicke/exechelper"
12 )
13
14 const (
15         logDir string = "/tmp/hs-test/"
16 )
17
18 var (
19         workDir, _ = os.Getwd()
20 )
21
22 type Volume struct {
23         hostDir          string
24         containerDir     string
25         isDefaultWorkDir bool
26 }
27
28 type Container struct {
29         suite            *HstSuite
30         isOptional       bool
31         runDetached      bool
32         name             string
33         image            string
34         extraRunningArgs string
35         volumes          map[string]Volume
36         envVars          map[string]string
37         vppInstance      *VppInstance
38 }
39
40 func newContainer(yamlInput ContainerConfig) (*Container, error) {
41         containerName := yamlInput["name"].(string)
42         if len(containerName) == 0 {
43                 err := fmt.Errorf("container name must not be blank")
44                 return nil, err
45         }
46
47         var container = new(Container)
48         container.volumes = make(map[string]Volume)
49         container.envVars = make(map[string]string)
50         container.name = containerName
51
52         if image, ok := yamlInput["image"]; ok {
53                 container.image = image.(string)
54         } else {
55                 container.image = "hs-test/vpp"
56         }
57
58         if args, ok := yamlInput["extra-args"]; ok {
59                 container.extraRunningArgs = args.(string)
60         } else {
61                 container.extraRunningArgs = ""
62         }
63
64         if isOptional, ok := yamlInput["is-optional"]; ok {
65                 container.isOptional = isOptional.(bool)
66         } else {
67                 container.isOptional = false
68         }
69
70         if runDetached, ok := yamlInput["run-detached"]; ok {
71                 container.runDetached = runDetached.(bool)
72         } else {
73                 container.runDetached = true
74         }
75
76         if _, ok := yamlInput["volumes"]; ok {
77                 r := strings.NewReplacer("$HST_DIR", workDir)
78                 for _, volu := range yamlInput["volumes"].([]interface{}) {
79                         volumeMap := volu.(ContainerConfig)
80                         hostDir := r.Replace(volumeMap["host-dir"].(string))
81                         containerDir := volumeMap["container-dir"].(string)
82                         isDefaultWorkDir := false
83
84                         if isDefault, ok := volumeMap["is-default-work-dir"]; ok {
85                                 isDefaultWorkDir = isDefault.(bool)
86                         }
87
88                         container.addVolume(hostDir, containerDir, isDefaultWorkDir)
89
90                 }
91         }
92
93         if _, ok := yamlInput["vars"]; ok {
94                 for _, envVar := range yamlInput["vars"].([]interface{}) {
95                         envVarMap := envVar.(ContainerConfig)
96                         name := envVarMap["name"].(string)
97                         value := envVarMap["value"].(string)
98                         container.addEnvVar(name, value)
99                 }
100         }
101         return container, nil
102 }
103
104 func (c *Container) getWorkDirVolume() (res Volume, exists bool) {
105         for _, v := range c.volumes {
106                 if v.isDefaultWorkDir {
107                         res = v
108                         exists = true
109                         return
110                 }
111         }
112         return
113 }
114
115 func (c *Container) getHostWorkDir() (res string) {
116         if v, ok := c.getWorkDirVolume(); ok {
117                 res = v.hostDir
118         }
119         return
120 }
121
122 func (c *Container) getContainerWorkDir() (res string) {
123         if v, ok := c.getWorkDirVolume(); ok {
124                 res = v.containerDir
125         }
126         return
127 }
128
129 func (c *Container) getContainerArguments() string {
130         args := "--ulimit nofile=90000:90000 --cap-add=all --privileged --network host --rm"
131         args += c.getVolumesAsCliOption()
132         args += c.getEnvVarsAsCliOption()
133         if *vppSourceFileDir != "" {
134                 args += fmt.Sprintf(" -v %s:%s", *vppSourceFileDir, *vppSourceFileDir)
135         }
136         args += " --name " + c.name + " " + c.image
137         args += " " + c.extraRunningArgs
138         return args
139 }
140
141 func (c *Container) runWithRetry(cmd string) error {
142         nTries := 5
143         for i := 0; i < nTries; i++ {
144                 err := exechelper.Run(cmd)
145                 if err == nil {
146                         return nil
147                 }
148                 time.Sleep(1 * time.Second)
149         }
150         return fmt.Errorf("failed to run container command")
151 }
152
153 func (c *Container) create() error {
154         cmd := "docker create " + c.getContainerArguments()
155         c.suite.log(cmd)
156         return exechelper.Run(cmd)
157 }
158
159 func (c *Container) start() error {
160         cmd := "docker start " + c.name
161         c.suite.log(cmd)
162         return c.runWithRetry(cmd)
163 }
164
165 func (c *Container) prepareCommand() (string, error) {
166         if c.name == "" {
167                 return "", fmt.Errorf("run container failed: name is blank")
168         }
169
170         cmd := "docker run "
171         if c.runDetached {
172                 cmd += " -d"
173         }
174         cmd += " " + c.getContainerArguments()
175
176         c.suite.log(cmd)
177         return cmd, nil
178 }
179
180 func (c *Container) combinedOutput() (string, error) {
181         cmd, err := c.prepareCommand()
182         if err != nil {
183                 return "", err
184         }
185
186         byteOutput, err := exechelper.CombinedOutput(cmd)
187         return string(byteOutput), err
188 }
189
190 func (c *Container) run() error {
191         cmd, err := c.prepareCommand()
192         if err != nil {
193                 return err
194         }
195         return c.runWithRetry(cmd)
196 }
197
198 func (c *Container) addVolume(hostDir string, containerDir string, isDefaultWorkDir bool) {
199         var volume Volume
200         volume.hostDir = hostDir
201         volume.containerDir = containerDir
202         volume.isDefaultWorkDir = isDefaultWorkDir
203         c.volumes[hostDir] = volume
204 }
205
206 func (c *Container) getVolumesAsCliOption() string {
207         cliOption := ""
208
209         if len(c.volumes) > 0 {
210                 for _, volume := range c.volumes {
211                         cliOption += fmt.Sprintf(" -v %s:%s", volume.hostDir, volume.containerDir)
212                 }
213         }
214
215         return cliOption
216 }
217
218 func (c *Container) addEnvVar(name string, value string) {
219         c.envVars[name] = value
220 }
221
222 func (c *Container) getEnvVarsAsCliOption() string {
223         cliOption := ""
224         if len(c.envVars) == 0 {
225                 return cliOption
226         }
227
228         for name, value := range c.envVars {
229                 cliOption += fmt.Sprintf(" -e %s=%s", name, value)
230         }
231         return cliOption
232 }
233
234 func (c *Container) newVppInstance(cpus []int, additionalConfigs ...Stanza) (*VppInstance, error) {
235         vpp := new(VppInstance)
236         vpp.container = c
237         vpp.cpus = cpus
238         vpp.additionalConfig = append(vpp.additionalConfig, additionalConfigs...)
239         c.vppInstance = vpp
240         return vpp, nil
241 }
242
243 func (c *Container) copy(sourceFileName string, targetFileName string) error {
244         cmd := exec.Command("docker", "cp", sourceFileName, c.name+":"+targetFileName)
245         return cmd.Run()
246 }
247
248 func (c *Container) createFile(destFileName string, content string) error {
249         f, err := os.CreateTemp("/tmp", "hst-config")
250         if err != nil {
251                 return err
252         }
253         defer os.Remove(f.Name())
254
255         if _, err := f.Write([]byte(content)); err != nil {
256                 return err
257         }
258         if err := f.Close(); err != nil {
259                 return err
260         }
261         c.copy(f.Name(), destFileName)
262         return nil
263 }
264
265 /*
266  * Executes in detached mode so that the started application can continue to run
267  * without blocking execution of test
268  */
269 func (c *Container) execServer(command string, arguments ...any) {
270         serverCommand := fmt.Sprintf(command, arguments...)
271         containerExecCommand := "docker exec -d" + c.getEnvVarsAsCliOption() +
272                 " " + c.name + " " + serverCommand
273         c.suite.T().Helper()
274         c.suite.log(containerExecCommand)
275         c.suite.assertNil(exechelper.Run(containerExecCommand))
276 }
277
278 func (c *Container) exec(command string, arguments ...any) string {
279         cliCommand := fmt.Sprintf(command, arguments...)
280         containerExecCommand := "docker exec" + c.getEnvVarsAsCliOption() +
281                 " " + c.name + " " + cliCommand
282         c.suite.T().Helper()
283         c.suite.log(containerExecCommand)
284         byteOutput, err := exechelper.CombinedOutput(containerExecCommand)
285         c.suite.assertNil(err)
286         return string(byteOutput)
287 }
288
289 func (c *Container) getLogDirPath() string {
290         testId := c.suite.getTestId()
291         testName := c.suite.T().Name()
292         logDirPath := logDir + testName + "/" + testId + "/"
293
294         cmd := exec.Command("mkdir", "-p", logDirPath)
295         if err := cmd.Run(); err != nil {
296                 c.suite.T().Fatalf("mkdir error: %v", err)
297         }
298
299         return logDirPath
300 }
301
302 func (c *Container) saveLogs() {
303         cmd := exec.Command("docker", "inspect", "--format='{{.State.Status}}'", c.name)
304         if output, _ := cmd.CombinedOutput(); !strings.Contains(string(output), "running") {
305                 return
306         }
307
308         testLogFilePath := c.getLogDirPath() + "container-" + c.name + ".log"
309
310         cmd = exec.Command("docker", "logs", "--details", "-t", c.name)
311         output, err := cmd.CombinedOutput()
312         if err != nil {
313                 c.suite.T().Fatalf("fetching logs error: %v", err)
314         }
315
316         f, err := os.Create(testLogFilePath)
317         if err != nil {
318                 c.suite.T().Fatalf("file create error: %v", err)
319         }
320         fmt.Fprint(f, string(output))
321         f.Close()
322 }
323
324 func (c *Container) log() string {
325         cmd := "docker logs " + c.name
326         c.suite.log(cmd)
327         o, err := exechelper.CombinedOutput(cmd)
328         c.suite.assertNil(err)
329         return string(o)
330 }
331
332 func (c *Container) stop() error {
333         if c.vppInstance != nil && c.vppInstance.apiChannel != nil {
334                 c.vppInstance.saveLogs()
335                 c.vppInstance.disconnect()
336         }
337         c.vppInstance = nil
338         c.saveLogs()
339         return exechelper.Run("docker stop " + c.name + " -t 0")
340 }
341
342 func (c *Container) createConfig(targetConfigName string, templateName string, values any) {
343         template := template.Must(template.ParseFiles(templateName))
344
345         f, err := os.CreateTemp("/tmp/hs-test/", "hst-config")
346         c.suite.assertNil(err)
347         defer os.Remove(f.Name())
348
349         err = template.Execute(f, values)
350         c.suite.assertNil(err)
351
352         err = f.Close()
353         c.suite.assertNil(err)
354
355         c.copy(f.Name(), targetConfigName)
356 }
357
358 func init() {
359         cmd := exec.Command("mkdir", "-p", logDir)
360         if err := cmd.Run(); err != nil {
361                 panic(err)
362         }
363 }