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