hs-test: configure VPP from test context
[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) getWorkDirVolume() (res Volume, exists bool) {
88         for _, v := range c.volumes {
89                 if v.isDefaultWorkDir {
90                         res = v
91                         exists = true
92                         return
93                 }
94         }
95         return
96 }
97
98 func (c *Container) GetHostWorkDir() (res string) {
99         if v, ok := c.getWorkDirVolume(); ok {
100                 res = v.hostDir
101         }
102         return
103 }
104
105 func (c *Container) GetContainerWorkDir() (res string) {
106         if v, ok := c.getWorkDirVolume(); ok {
107                 res = v.containerDir
108         }
109         return
110 }
111
112 func (c *Container) getRunCommand() string {
113         syncPath := fmt.Sprintf(" -v %s:/tmp/sync", c.getSyncPath())
114         cmd := "docker run --cap-add=all -d --privileged --network host --rm"
115         cmd += syncPath
116         cmd += c.getVolumesAsCliOption()
117         cmd += c.getEnvVarsAsCliOption()
118         cmd += " --name " + c.name + " " + c.image + " " + c.extraRunningArgs
119         return cmd
120 }
121
122 func (c *Container) run() error {
123         if c.name == "" {
124                 return fmt.Errorf("run container failed: name is blank")
125         }
126
127         exechelper.Run(fmt.Sprintf("mkdir -p /tmp/%s/sync", c.name))
128         cmd := c.getRunCommand()
129         err := exechelper.Run(cmd)
130         if err != nil {
131                 return fmt.Errorf("container run failed: %s", err)
132         }
133
134         return nil
135 }
136
137 func (c *Container) addVolume(hostDir string, containerDir string, isDefaultWorkDir bool) {
138         var volume Volume
139         volume.hostDir = hostDir
140         volume.containerDir = containerDir
141         volume.isDefaultWorkDir = isDefaultWorkDir
142         c.volumes[hostDir] = volume
143 }
144
145 func (c *Container) getVolumeByHostDir(hostDir string) Volume {
146         return c.volumes[hostDir]
147 }
148
149 func (c *Container) getVolumesAsCliOption() string {
150         cliOption := ""
151
152         if len(c.volumes) > 0 {
153                 for _, volume := range c.volumes {
154                         cliOption += fmt.Sprintf(" -v %s:%s", volume.hostDir, volume.containerDir)
155                 }
156         }
157
158         return cliOption
159 }
160
161 func (c *Container) getWorkDirAsCliOption() string {
162         if _, ok := c.getWorkDirVolume(); ok {
163                 return fmt.Sprintf(" --workdir=\"%s\"", c.GetContainerWorkDir())
164         }
165         return ""
166 }
167
168 func (c *Container) addEnvVar(name string, value string) {
169         c.envVars[name] = value
170 }
171
172 func (c *Container) getEnvVarsAsCliOption() string {
173         cliOption := ""
174         if len(c.envVars) == 0 {
175                 return cliOption
176         }
177
178         for name, value := range c.envVars {
179                 cliOption += fmt.Sprintf(" -e %s=%s", name, value)
180         }
181         return cliOption
182 }
183
184 func (c *Container) getSyncPath() string {
185         return fmt.Sprintf("/tmp/%s/sync", c.name)
186 }
187
188 func (c *Container) newVppInstance(additionalConfig ...Stanza) (*VppInstance, error) {
189         vppConfig := new(VppConfig)
190         vppConfig.CliSocketFilePath = defaultCliSocketFilePath
191         if len(additionalConfig) > 0 {
192                 vppConfig.additionalConfig = additionalConfig[0]
193         }
194
195         vpp := new(VppInstance)
196         vpp.container = c
197         vpp.config = vppConfig
198
199         c.vppInstance = vpp
200
201         return vpp, nil
202 }
203
204 func (c *Container) copy(sourceFileName string, targetFileName string) error {
205         cmd := exec.Command("docker", "cp", sourceFileName, c.name+":"+targetFileName)
206         return cmd.Run()
207 }
208
209 func (c *Container) createFile(destFileName string, content string) error {
210         f, err := os.CreateTemp("/tmp", "hst-config")
211         if err != nil {
212                 return err
213         }
214         defer os.Remove(f.Name())
215
216         if _, err := f.Write([]byte(content)); err != nil {
217                 return err
218         }
219         if err := f.Close(); err != nil {
220                 return err
221         }
222         c.copy(f.Name(), destFileName)
223         return nil
224 }
225
226 /*
227  * Executes in detached mode so that the started application can continue to run
228  * without blocking execution of test
229  */
230 func (c *Container) execServer(command string) error {
231         return exechelper.Run("docker exec -d" + c.getEnvVarsAsCliOption() + " " + c.name + " " + command)
232 }
233
234 func (c *Container) exec(command string) (string, error) {
235         cliCommand := "docker exec" + c.getEnvVarsAsCliOption() + " " + c.name + " " + command
236         byteOutput, err := exechelper.CombinedOutput(cliCommand)
237         return string(byteOutput), err
238 }
239
240 func (c *Container) execAction(args string) (string, error) {
241         syncFile := c.getSyncPath() + "/rc"
242         os.Remove(syncFile)
243
244         workDir := c.getWorkDirAsCliOption()
245         cmd := fmt.Sprintf("docker exec -d %s %s hs-test %s",
246                 workDir,
247                 c.name,
248                 args)
249         err := exechelper.Run(cmd)
250         if err != nil {
251                 return "", err
252         }
253         res, err := waitForSyncFile(syncFile)
254         if err != nil {
255                 return "", fmt.Errorf("failed to read sync file while executing 'hs-test %s': %v", args, err)
256         }
257         o := res.StdOutput + res.ErrOutput
258         if res.Code != 0 {
259                 return o, fmt.Errorf("cmd resulted in non-zero value %d: %s", res.Code, res.Desc)
260         }
261         return o, err
262 }
263
264 func (c *Container) stop() error {
265         if c.vppInstance != nil && c.vppInstance.apiChannel != nil {
266                 c.vppInstance.disconnect()
267         }
268         c.vppInstance = nil
269         return exechelper.Run("docker stop " + c.name + " -t 0")
270 }