hs-test: refactor test cases from ns suite
[vpp.git] / extras / hs-test / container.go
1 package main
2
3 import (
4         "fmt"
5         "os"
6         "os/exec"
7         "strings"
8
9         "github.com/edwarnicke/exechelper"
10 )
11
12 type Volume struct {
13         hostDir          string
14         containerDir     string
15         isDefaultWorkDir bool
16 }
17
18 type Container struct {
19         suite            *HstSuite
20         isOptional       bool
21         name             string
22         image            string
23         extraRunningArgs string
24         volumes          map[string]Volume
25         envVars          map[string]string
26         vppInstance      *VppInstance
27 }
28
29 func NewContainer(yamlInput ContainerConfig) (*Container, error) {
30         containerName := yamlInput["name"].(string)
31         if len(containerName) == 0 {
32                 err := fmt.Errorf("container name must not be blank")
33                 return nil, err
34         }
35
36         var container = new(Container)
37         container.volumes = make(map[string]Volume)
38         container.envVars = make(map[string]string)
39         container.name = containerName
40
41         if image, ok := yamlInput["image"]; ok {
42                 container.image = image.(string)
43         } else {
44                 container.image = "hs-test/vpp"
45         }
46
47         if args, ok := yamlInput["extra-args"]; ok {
48                 container.extraRunningArgs = args.(string)
49         } else {
50                 container.extraRunningArgs = ""
51         }
52
53         if isOptional, ok := yamlInput["is-optional"]; ok {
54                 container.isOptional = isOptional.(bool)
55         } else {
56                 container.isOptional = false
57         }
58
59         if _, ok := yamlInput["volumes"]; ok {
60                 r := strings.NewReplacer("$HST_DIR", workDir)
61                 for _, volu := range yamlInput["volumes"].([]interface{}) {
62                         volumeMap := volu.(ContainerConfig)
63                         hostDir := r.Replace(volumeMap["host-dir"].(string))
64                         containerDir := volumeMap["container-dir"].(string)
65                         isDefaultWorkDir := false
66
67                         if isDefault, ok := volumeMap["is-default-work-dir"]; ok {
68                                 isDefaultWorkDir = isDefault.(bool)
69                         }
70
71                         container.addVolume(hostDir, containerDir, isDefaultWorkDir)
72
73                 }
74         }
75
76         if _, ok := yamlInput["vars"]; ok {
77                 for _, envVar := range yamlInput["vars"].([]interface{}) {
78                         envVarMap := envVar.(ContainerConfig)
79                         name := envVarMap["name"].(string)
80                         value := envVarMap["value"].(string)
81                         container.addEnvVar(name, value)
82                 }
83         }
84         return container, nil
85 }
86
87 func (c *Container) Suite() *HstSuite {
88         return c.suite
89 }
90
91 func (c *Container) getWorkDirVolume() (res Volume, exists bool) {
92         for _, v := range c.volumes {
93                 if v.isDefaultWorkDir {
94                         res = v
95                         exists = true
96                         return
97                 }
98         }
99         return
100 }
101
102 func (c *Container) GetHostWorkDir() (res string) {
103         if v, ok := c.getWorkDirVolume(); ok {
104                 res = v.hostDir
105         }
106         return
107 }
108
109 func (c *Container) GetContainerWorkDir() (res string) {
110         if v, ok := c.getWorkDirVolume(); ok {
111                 res = v.containerDir
112         }
113         return
114 }
115
116 func (c *Container) getRunCommand() string {
117         syncPath := fmt.Sprintf(" -v %s:/tmp/sync", c.getSyncPath())
118         cmd := "docker run --cap-add=all -d --privileged --network host --rm"
119         cmd += syncPath
120         cmd += c.getVolumesAsCliOption()
121         cmd += c.getEnvVarsAsCliOption()
122         cmd += " --name " + c.name + " " + c.image + " " + c.extraRunningArgs
123         return cmd
124 }
125
126 func (c *Container) run() error {
127         if c.name == "" {
128                 return fmt.Errorf("run container failed: name is blank")
129         }
130
131         exechelper.Run(fmt.Sprintf("mkdir -p /tmp/%s/sync", c.name))
132         cmd := c.getRunCommand()
133         err := exechelper.Run(cmd)
134         if err != nil {
135                 return fmt.Errorf("container run failed: %s", err)
136         }
137
138         return nil
139 }
140
141 func (c *Container) addVolume(hostDir string, containerDir string, isDefaultWorkDir bool) {
142         var volume Volume
143         volume.hostDir = hostDir
144         volume.containerDir = containerDir
145         volume.isDefaultWorkDir = isDefaultWorkDir
146         c.volumes[hostDir] = volume
147 }
148
149 func (c *Container) getVolumeByHostDir(hostDir string) Volume {
150         return c.volumes[hostDir]
151 }
152
153 func (c *Container) getVolumesAsCliOption() string {
154         cliOption := ""
155
156         if len(c.volumes) > 0 {
157                 for _, volume := range c.volumes {
158                         cliOption += fmt.Sprintf(" -v %s:%s", volume.hostDir, volume.containerDir)
159                 }
160         }
161
162         return cliOption
163 }
164
165 func (c *Container) getWorkDirAsCliOption() string {
166         if _, ok := c.getWorkDirVolume(); ok {
167                 return fmt.Sprintf(" --workdir=\"%s\"", c.GetContainerWorkDir())
168         }
169         return ""
170 }
171
172 func (c *Container) addEnvVar(name string, value string) {
173         c.envVars[name] = value
174 }
175
176 func (c *Container) getEnvVarsAsCliOption() string {
177         cliOption := ""
178         if len(c.envVars) == 0 {
179                 return cliOption
180         }
181
182         for name, value := range c.envVars {
183                 cliOption += fmt.Sprintf(" -e %s=%s", name, value)
184         }
185         return cliOption
186 }
187
188 func (c *Container) getSyncPath() string {
189         return fmt.Sprintf("/tmp/%s/sync", c.name)
190 }
191
192 func (c *Container) newVppInstance(additionalConfig ...Stanza) (*VppInstance, error) {
193         vppConfig := new(VppConfig)
194         vppConfig.CliSocketFilePath = defaultCliSocketFilePath
195         if len(additionalConfig) > 0 {
196                 vppConfig.additionalConfig = additionalConfig[0]
197         }
198
199         vpp := new(VppInstance)
200         vpp.container = c
201         vpp.config = vppConfig
202
203         c.vppInstance = vpp
204
205         return vpp, nil
206 }
207
208 func (c *Container) copy(sourceFileName string, targetFileName string) error {
209         cmd := exec.Command("docker", "cp", sourceFileName, c.name+":"+targetFileName)
210         return cmd.Run()
211 }
212
213 func (c *Container) createFile(destFileName string, content string) error {
214         f, err := os.CreateTemp("/tmp", "hst-config")
215         if err != nil {
216                 return err
217         }
218         defer os.Remove(f.Name())
219
220         if _, err := f.Write([]byte(content)); err != nil {
221                 return err
222         }
223         if err := f.Close(); err != nil {
224                 return err
225         }
226         c.copy(f.Name(), destFileName)
227         return nil
228 }
229
230 /*
231  * Executes in detached mode so that the started application can continue to run
232  * without blocking execution of test
233  */
234 func (c *Container) execServer(command string, arguments ...any) {
235         serverCommand := fmt.Sprintf(command, arguments...)
236         containerExecCommand := "docker exec -d" + c.getEnvVarsAsCliOption() +
237                 " " + c.name + " " + serverCommand
238         c.Suite().log(containerExecCommand)
239         c.Suite().assertNil(exechelper.Run(containerExecCommand))
240 }
241
242 func (c *Container) exec(command string, arguments ...any) string {
243         cliCommand := fmt.Sprintf(command, arguments...)
244         containerExecCommand := "docker exec" + c.getEnvVarsAsCliOption() +
245                 " " + c.name + " " + cliCommand
246         c.Suite().log(containerExecCommand)
247         byteOutput, err := exechelper.CombinedOutput(containerExecCommand)
248         c.Suite().assertNil(err)
249         return string(byteOutput)
250 }
251
252 func (c *Container) execAction(args string) (string, error) {
253         syncFile := c.getSyncPath() + "/rc"
254         os.Remove(syncFile)
255
256         workDir := c.getWorkDirAsCliOption()
257         cmd := fmt.Sprintf("docker exec -d %s %s hs-test %s",
258                 workDir,
259                 c.name,
260                 args)
261         err := exechelper.Run(cmd)
262         if err != nil {
263                 return "", err
264         }
265         res, err := waitForSyncFile(syncFile)
266         if err != nil {
267                 return "", fmt.Errorf("failed to read sync file while executing 'hs-test %s': %v", args, err)
268         }
269         o := res.StdOutput + res.ErrOutput
270         if res.Code != 0 {
271                 return o, fmt.Errorf("cmd resulted in non-zero value %d: %s", res.Code, res.Desc)
272         }
273         return o, err
274 }
275
276 func (c *Container) stop() error {
277         if c.vppInstance != nil && c.vppInstance.apiChannel != nil {
278                 c.vppInstance.disconnect()
279         }
280         c.vppInstance = nil
281         return exechelper.Run("docker stop " + c.name + " -t 0")
282 }