1dc49b763a56225e97073f221be5c787d2c35f67
[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         args += " --name " + c.name + " " + c.image
133         args += " " + c.extraRunningArgs
134         return args
135 }
136
137 func (c *Container) create() error {
138         cmd := "docker create " + c.getContainerArguments()
139         c.suite.log(cmd)
140         return exechelper.Run(cmd)
141 }
142
143 func (c *Container) start() error {
144         cmd := "docker start " + c.name
145         c.suite.log(cmd)
146         return exechelper.Run(cmd)
147 }
148
149 func (c *Container) prepareCommand() (string, error) {
150         if c.name == "" {
151                 return "", fmt.Errorf("run container failed: name is blank")
152         }
153
154         cmd := "docker run "
155         if c.runDetached {
156                 cmd += " -d"
157         }
158         cmd += " " + c.getContainerArguments()
159
160         c.suite.log(cmd)
161         return cmd, nil
162 }
163
164 func (c *Container) combinedOutput() (string, error) {
165         cmd, err := c.prepareCommand()
166         if err != nil {
167                 return "", err
168         }
169
170         byteOutput, err := exechelper.CombinedOutput(cmd)
171         return string(byteOutput), err
172 }
173
174 func (c *Container) run() error {
175         cmd, err := c.prepareCommand()
176         if err != nil {
177                 return err
178         }
179
180         return exechelper.Run(cmd)
181 }
182
183 func (c *Container) addVolume(hostDir string, containerDir string, isDefaultWorkDir bool) {
184         var volume Volume
185         volume.hostDir = hostDir
186         volume.containerDir = containerDir
187         volume.isDefaultWorkDir = isDefaultWorkDir
188         c.volumes[hostDir] = volume
189 }
190
191 func (c *Container) getVolumesAsCliOption() string {
192         cliOption := ""
193
194         if len(c.volumes) > 0 {
195                 for _, volume := range c.volumes {
196                         cliOption += fmt.Sprintf(" -v %s:%s", volume.hostDir, volume.containerDir)
197                 }
198         }
199
200         return cliOption
201 }
202
203 func (c *Container) addEnvVar(name string, value string) {
204         c.envVars[name] = value
205 }
206
207 func (c *Container) getEnvVarsAsCliOption() string {
208         cliOption := ""
209         if len(c.envVars) == 0 {
210                 return cliOption
211         }
212
213         for name, value := range c.envVars {
214                 cliOption += fmt.Sprintf(" -e %s=%s", name, value)
215         }
216         return cliOption
217 }
218
219 func (c *Container) newVppInstance(additionalConfig ...Stanza) (*VppInstance, error) {
220         vpp := new(VppInstance)
221         vpp.container = c
222
223         if len(additionalConfig) > 0 {
224                 vpp.additionalConfig = additionalConfig[0]
225         }
226
227         c.vppInstance = vpp
228
229         return vpp, nil
230 }
231
232 func (c *Container) copy(sourceFileName string, targetFileName string) error {
233         cmd := exec.Command("docker", "cp", sourceFileName, c.name+":"+targetFileName)
234         return cmd.Run()
235 }
236
237 func (c *Container) createFile(destFileName string, content string) error {
238         f, err := os.CreateTemp("/tmp", "hst-config")
239         if err != nil {
240                 return err
241         }
242         defer os.Remove(f.Name())
243
244         if _, err := f.Write([]byte(content)); err != nil {
245                 return err
246         }
247         if err := f.Close(); err != nil {
248                 return err
249         }
250         c.copy(f.Name(), destFileName)
251         return nil
252 }
253
254 /*
255  * Executes in detached mode so that the started application can continue to run
256  * without blocking execution of test
257  */
258 func (c *Container) execServer(command string, arguments ...any) {
259         serverCommand := fmt.Sprintf(command, arguments...)
260         containerExecCommand := "docker exec -d" + c.getEnvVarsAsCliOption() +
261                 " " + c.name + " " + serverCommand
262         c.suite.T().Helper()
263         c.suite.log(containerExecCommand)
264         c.suite.assertNil(exechelper.Run(containerExecCommand))
265 }
266
267 func (c *Container) exec(command string, arguments ...any) string {
268         cliCommand := fmt.Sprintf(command, arguments...)
269         containerExecCommand := "docker exec" + c.getEnvVarsAsCliOption() +
270                 " " + c.name + " " + cliCommand
271         c.suite.T().Helper()
272         c.suite.log(containerExecCommand)
273         byteOutput, err := exechelper.CombinedOutput(containerExecCommand)
274         c.suite.assertNil(err)
275         return string(byteOutput)
276 }
277
278 func (c *Container) getLogDirPath() string {
279         testId := c.suite.getTestId()
280         testName := c.suite.T().Name()
281         logDirPath := logDir + testName + "/" + testId + "/"
282
283         cmd := exec.Command("mkdir", "-p", logDirPath)
284         if err := cmd.Run(); err != nil {
285                 c.suite.T().Fatalf("mkdir error: %v", err)
286         }
287
288         return logDirPath
289 }
290
291 func (c *Container) saveLogs() {
292         cmd := exec.Command("docker", "inspect", "--format='{{.State.Status}}'", c.name)
293         if output, _ := cmd.CombinedOutput(); !strings.Contains(string(output), "running") {
294                 return
295         }
296
297         testLogFilePath := c.getLogDirPath() + "container-" + c.name + ".log"
298
299         cmd = exec.Command("docker", "logs", "--details", "-t", c.name)
300         output, err := cmd.CombinedOutput()
301         if err != nil {
302                 c.suite.T().Fatalf("fetching logs error: %v", err)
303         }
304
305         f, err := os.Create(testLogFilePath)
306         if err != nil {
307                 c.suite.T().Fatalf("file create error: %v", err)
308         }
309         fmt.Fprint(f, string(output))
310         f.Close()
311 }
312
313 func (c *Container) log() string {
314         cmd := "docker logs " + c.name
315         c.suite.log(cmd)
316         o, err := exechelper.CombinedOutput(cmd)
317         c.suite.assertNil(err)
318         return string(o)
319 }
320
321 func (c *Container) stop() error {
322         if c.vppInstance != nil && c.vppInstance.apiChannel != nil {
323                 c.vppInstance.saveLogs()
324                 c.vppInstance.disconnect()
325         }
326         c.vppInstance = nil
327         c.saveLogs()
328         return exechelper.Run("docker stop " + c.name + " -t 0")
329 }
330
331 func (c *Container) createConfig(targetConfigName string, templateName string, values any) {
332         template := template.Must(template.ParseFiles(templateName))
333
334         f, err := os.CreateTemp("/tmp/hs-test/", "hst-config")
335         c.suite.assertNil(err)
336         defer os.Remove(f.Name())
337
338         err = template.Execute(f, values)
339         c.suite.assertNil(err)
340
341         err = f.Close()
342         c.suite.assertNil(err)
343
344         c.copy(f.Name(), targetConfigName)
345 }
346
347 func init() {
348         cmd := exec.Command("mkdir", "-p", logDir)
349         if err := cmd.Run(); err != nil {
350                 panic(err)
351         }
352 }