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