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