fd3aa47b21bf75f51a1c7c3e73f80bc10618b979
[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(cpus []int, additionalConfigs ...Stanza) (*VppInstance, error) {
220         vpp := new(VppInstance)
221         vpp.container = c
222         vpp.cpus = cpus
223         vpp.additionalConfig = append(vpp.additionalConfig, additionalConfigs...)
224         c.vppInstance = vpp
225         return vpp, nil
226 }
227
228 func (c *Container) copy(sourceFileName string, targetFileName string) error {
229         cmd := exec.Command("docker", "cp", sourceFileName, c.name+":"+targetFileName)
230         return cmd.Run()
231 }
232
233 func (c *Container) createFile(destFileName string, content string) error {
234         f, err := os.CreateTemp("/tmp", "hst-config")
235         if err != nil {
236                 return err
237         }
238         defer os.Remove(f.Name())
239
240         if _, err := f.Write([]byte(content)); err != nil {
241                 return err
242         }
243         if err := f.Close(); err != nil {
244                 return err
245         }
246         c.copy(f.Name(), destFileName)
247         return nil
248 }
249
250 /*
251  * Executes in detached mode so that the started application can continue to run
252  * without blocking execution of test
253  */
254 func (c *Container) execServer(command string, arguments ...any) {
255         serverCommand := fmt.Sprintf(command, arguments...)
256         containerExecCommand := "docker exec -d" + c.getEnvVarsAsCliOption() +
257                 " " + c.name + " " + serverCommand
258         c.suite.T().Helper()
259         c.suite.log(containerExecCommand)
260         c.suite.assertNil(exechelper.Run(containerExecCommand))
261 }
262
263 func (c *Container) exec(command string, arguments ...any) string {
264         cliCommand := fmt.Sprintf(command, arguments...)
265         containerExecCommand := "docker exec" + c.getEnvVarsAsCliOption() +
266                 " " + c.name + " " + cliCommand
267         c.suite.T().Helper()
268         c.suite.log(containerExecCommand)
269         byteOutput, err := exechelper.CombinedOutput(containerExecCommand)
270         c.suite.assertNil(err)
271         return string(byteOutput)
272 }
273
274 func (c *Container) getLogDirPath() string {
275         testId := c.suite.getTestId()
276         testName := c.suite.T().Name()
277         logDirPath := logDir + testName + "/" + testId + "/"
278
279         cmd := exec.Command("mkdir", "-p", logDirPath)
280         if err := cmd.Run(); err != nil {
281                 c.suite.T().Fatalf("mkdir error: %v", err)
282         }
283
284         return logDirPath
285 }
286
287 func (c *Container) saveLogs() {
288         cmd := exec.Command("docker", "inspect", "--format='{{.State.Status}}'", c.name)
289         if output, _ := cmd.CombinedOutput(); !strings.Contains(string(output), "running") {
290                 return
291         }
292
293         testLogFilePath := c.getLogDirPath() + "container-" + c.name + ".log"
294
295         cmd = exec.Command("docker", "logs", "--details", "-t", c.name)
296         output, err := cmd.CombinedOutput()
297         if err != nil {
298                 c.suite.T().Fatalf("fetching logs error: %v", err)
299         }
300
301         f, err := os.Create(testLogFilePath)
302         if err != nil {
303                 c.suite.T().Fatalf("file create error: %v", err)
304         }
305         fmt.Fprint(f, string(output))
306         f.Close()
307 }
308
309 func (c *Container) log() string {
310         cmd := "docker logs " + c.name
311         c.suite.log(cmd)
312         o, err := exechelper.CombinedOutput(cmd)
313         c.suite.assertNil(err)
314         return string(o)
315 }
316
317 func (c *Container) stop() error {
318         if c.vppInstance != nil && c.vppInstance.apiChannel != nil {
319                 c.vppInstance.saveLogs()
320                 c.vppInstance.disconnect()
321         }
322         c.vppInstance = nil
323         c.saveLogs()
324         return exechelper.Run("docker stop " + c.name + " -t 0")
325 }
326
327 func (c *Container) createConfig(targetConfigName string, templateName string, values any) {
328         template := template.Must(template.ParseFiles(templateName))
329
330         f, err := os.CreateTemp("/tmp/hs-test/", "hst-config")
331         c.suite.assertNil(err)
332         defer os.Remove(f.Name())
333
334         err = template.Execute(f, values)
335         c.suite.assertNil(err)
336
337         err = f.Close()
338         c.suite.assertNil(err)
339
340         c.copy(f.Name(), targetConfigName)
341 }
342
343 func init() {
344         cmd := exec.Command("mkdir", "-p", logDir)
345         if err := cmd.Run(); err != nil {
346                 panic(err)
347         }
348 }