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