github.com/opencontainers/runtime-tools@v0.9.0/validation/util/test.go (about) 1 package util 2 3 import ( 4 "errors" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "os/exec" 9 "path/filepath" 10 "runtime" 11 "strings" 12 "time" 13 14 "github.com/mndrix/tap-go" 15 "github.com/mrunalp/fileutils" 16 rspec "github.com/opencontainers/runtime-spec/specs-go" 17 "github.com/opencontainers/runtime-tools/generate" 18 "github.com/opencontainers/runtime-tools/specerror" 19 "github.com/satori/go.uuid" 20 ) 21 22 var ( 23 // RuntimeCommand is the default runtime command. 24 RuntimeCommand = "runc" 25 ) 26 27 // LifecycleAction defines the phases will be called. 28 type LifecycleAction int 29 30 const ( 31 // LifecycleActionNone does nothing 32 LifecycleActionNone = 0 33 // LifecycleActionCreate creates a container 34 LifecycleActionCreate = 1 << iota 35 // LifecycleActionStart starts a container 36 LifecycleActionStart 37 // LifecycleActionDelete deletes a container 38 LifecycleActionDelete 39 ) 40 41 // LifecycleStatus follows https://github.com/opencontainers/runtime-spec/blob/master/runtime.md#state 42 type LifecycleStatus int 43 44 const ( 45 // LifecycleStatusCreating "creating" 46 LifecycleStatusCreating = 1 << iota 47 // LifecycleStatusCreated "created" 48 LifecycleStatusCreated 49 // LifecycleStatusRunning "running" 50 LifecycleStatusRunning 51 // LifecycleStatusStopped "stopped" 52 LifecycleStatusStopped 53 ) 54 55 var lifecycleStatusMap = map[string]LifecycleStatus{ 56 "creating": LifecycleStatusCreating, 57 "created": LifecycleStatusCreated, 58 "running": LifecycleStatusRunning, 59 "stopped": LifecycleStatusStopped, 60 } 61 62 // LifecycleConfig includes 63 // 1. Config to set the 'config.json' 64 // 2. BundleDir to set the bundle directory 65 // 3. Actions to define the default running lifecycles 66 // 4. Four phases for user to add his/her own operations 67 type LifecycleConfig struct { 68 Config *generate.Generator 69 BundleDir string 70 Actions LifecycleAction 71 PreCreate func(runtime *Runtime) error 72 PostCreate func(runtime *Runtime) error 73 PreDelete func(runtime *Runtime) error 74 PostDelete func(runtime *Runtime) error 75 } 76 77 // PreFunc initializes the test environment after preparing the bundle 78 // but before creating the container. 79 type PreFunc func(string) error 80 81 // AfterFunc validate container's outside environment after created 82 type AfterFunc func(config *rspec.Spec, t *tap.T, state *rspec.State) error 83 84 func init() { 85 runtimeInEnv := os.Getenv("RUNTIME") 86 if runtimeInEnv != "" { 87 RuntimeCommand = runtimeInEnv 88 } 89 } 90 91 // Fatal prints a warning to stderr and exits. 92 func Fatal(err error) { 93 fmt.Fprintf(os.Stderr, "%+v\n", err) 94 os.Exit(1) 95 } 96 97 // Skip skips a full TAP suite. 98 func Skip(message string, diagnostic interface{}) { 99 t := tap.New() 100 t.Header(1) 101 t.Skip(1, message) 102 if diagnostic != nil { 103 t.YAML(diagnostic) 104 } 105 } 106 107 // SpecErrorOK generates TAP output indicating whether a spec code test passed or failed. 108 func SpecErrorOK(t *tap.T, expected bool, specErr error, detailedErr error) { 109 t.Ok(expected, specErr.(*specerror.Error).Err.Err.Error()) 110 diagnostic := map[string]string{ 111 "reference": specErr.(*specerror.Error).Err.Reference, 112 } 113 114 if detailedErr != nil { 115 diagnostic["error"] = detailedErr.Error() 116 if e, ok := detailedErr.(*exec.ExitError); ok { 117 if len(e.Stderr) > 0 { 118 diagnostic["stderr"] = string(e.Stderr) 119 } 120 } 121 } 122 t.YAML(diagnostic) 123 } 124 125 // PrepareBundle creates a test bundle in a temporary directory. 126 func PrepareBundle() (string, error) { 127 bundleDir, err := ioutil.TempDir("", "ocitest") 128 if err != nil { 129 return "", err 130 } 131 132 // Untar the root fs 133 untarCmd := exec.Command("tar", "-xf", fmt.Sprintf("rootfs-%s.tar.gz", runtime.GOARCH), "-C", bundleDir) 134 output, err := untarCmd.CombinedOutput() 135 if err != nil { 136 os.Stderr.Write(output) 137 os.RemoveAll(bundleDir) 138 return "", err 139 } 140 141 return bundleDir, nil 142 } 143 144 // GetDefaultGenerator creates a default configuration generator. 145 func GetDefaultGenerator() (*generate.Generator, error) { 146 g, err := generate.New(runtime.GOOS) 147 if err != nil { 148 return nil, err 149 } 150 g.SetRootPath(".") 151 g.SetProcessArgs([]string{"/runtimetest", "--path=/"}) 152 return &g, err 153 } 154 155 // WaitingForStatus waits an expected runtime status, return error if 156 // 1. fail to query the status 157 // 2. timeout 158 func WaitingForStatus(r Runtime, status LifecycleStatus, retryTimeout time.Duration, pollInterval time.Duration) error { 159 for start := time.Now(); time.Since(start) < retryTimeout; time.Sleep(pollInterval) { 160 state, err := r.State() 161 if err != nil { 162 return err 163 } 164 if v, ok := lifecycleStatusMap[state.Status]; ok { 165 if status&v != 0 { 166 return nil 167 } 168 } else { 169 // In spec, it says 'Additional values MAY be defined by the runtime'. 170 continue 171 } 172 } 173 174 return errors.New("timeout in waiting for the container status") 175 } 176 177 var runtimeInsideValidateCalled bool 178 179 // RuntimeInsideValidate runs runtimetest inside a container. 180 func RuntimeInsideValidate(g *generate.Generator, t *tap.T, f PreFunc) (err error) { 181 bundleDir, err := PrepareBundle() 182 if err != nil { 183 return err 184 } 185 186 if f != nil { 187 if err := f(bundleDir); err != nil { 188 return err 189 } 190 } 191 192 r, err := NewRuntime(RuntimeCommand, bundleDir) 193 if err != nil { 194 os.RemoveAll(bundleDir) 195 return err 196 } 197 defer r.Clean(true, true) 198 err = r.SetConfig(g) 199 if err != nil { 200 return err 201 } 202 err = fileutils.CopyFile("runtimetest", filepath.Join(r.BundleDir, "runtimetest")) 203 if err != nil { 204 return err 205 } 206 207 r.SetID(uuid.NewV4().String()) 208 err = r.Create() 209 if err != nil { 210 os.Stderr.WriteString("failed to create the container\n") 211 if e, ok := err.(*exec.ExitError); ok && len(e.Stderr) > 0 { 212 os.Stderr.Write(e.Stderr) 213 } 214 return err 215 } 216 217 err = r.Start() 218 if err != nil { 219 os.Stderr.WriteString("failed to start the container\n") 220 if e, ok := err.(*exec.ExitError); ok && len(e.Stderr) > 0 { 221 os.Stderr.Write(e.Stderr) 222 } 223 return err 224 } 225 226 err = WaitingForStatus(r, LifecycleStatusStopped, 10*time.Second, 1*time.Second) 227 if err != nil { 228 return err 229 } 230 231 stdout, stderr, err := r.ReadStandardStreams() 232 if err != nil { 233 if len(stderr) == 0 { 234 stderr = stdout 235 } 236 os.Stderr.WriteString("failed to read standard streams\n") 237 os.Stderr.Write(stderr) 238 return err 239 } 240 241 // Write stdout in the outter TAP 242 if t != nil { 243 diagnostic := map[string]string{ 244 "stdout": string(stdout), 245 "stderr": string(stderr), 246 } 247 if err != nil { 248 diagnostic["error"] = fmt.Sprintf("%v", err) 249 } 250 t.YAML(diagnostic) 251 t.Ok(err == nil && !strings.Contains(string(stdout), "not ok"), g.Config.Annotations["TestName"]) 252 } else { 253 if runtimeInsideValidateCalled { 254 Fatal(errors.New("RuntimeInsideValidate called several times in the same test without passing TAP")) 255 } 256 runtimeInsideValidateCalled = true 257 os.Stdout.Write(stdout) 258 } 259 return nil 260 } 261 262 // RuntimeOutsideValidate validate runtime outside a container. 263 func RuntimeOutsideValidate(g *generate.Generator, t *tap.T, f AfterFunc) error { 264 bundleDir, err := PrepareBundle() 265 if err != nil { 266 return err 267 } 268 269 r, err := NewRuntime(RuntimeCommand, bundleDir) 270 if err != nil { 271 os.RemoveAll(bundleDir) 272 return err 273 } 274 defer r.Clean(true, true) 275 err = r.SetConfig(g) 276 if err != nil { 277 return err 278 } 279 err = fileutils.CopyFile("runtimetest", filepath.Join(r.BundleDir, "runtimetest")) 280 if err != nil { 281 return err 282 } 283 284 r.SetID(uuid.NewV4().String()) 285 err = r.Create() 286 if err != nil { 287 os.Stderr.WriteString("failed to create the container\n") 288 if e, ok := err.(*exec.ExitError); ok && len(e.Stderr) > 0 { 289 os.Stderr.Write(e.Stderr) 290 } 291 return err 292 } 293 294 if f != nil { 295 state, err := r.State() 296 if err != nil { 297 return err 298 } 299 if err := f(g.Spec(), t, &state); err != nil { 300 return err 301 } 302 } 303 return nil 304 } 305 306 // RuntimeLifecycleValidate validates runtime lifecycle. 307 func RuntimeLifecycleValidate(config LifecycleConfig) error { 308 var bundleDir string 309 var err error 310 311 if config.BundleDir == "" { 312 bundleDir, err = PrepareBundle() 313 if err != nil { 314 return err 315 } 316 defer os.RemoveAll(bundleDir) 317 } else { 318 bundleDir = config.BundleDir 319 } 320 321 r, err := NewRuntime(RuntimeCommand, bundleDir) 322 if err != nil { 323 return err 324 } 325 326 if config.Config != nil { 327 if err := r.SetConfig(config.Config); err != nil { 328 return err 329 } 330 } 331 332 if config.PreCreate != nil { 333 if err := config.PreCreate(&r); err != nil { 334 return err 335 } 336 } 337 338 if config.Actions&LifecycleActionCreate != 0 { 339 err := r.Create() 340 if err != nil { 341 os.Stderr.WriteString("failed to create the container\n") 342 if e, ok := err.(*exec.ExitError); ok && len(e.Stderr) > 0 { 343 os.Stderr.Write(e.Stderr) 344 } 345 return err 346 } 347 if config.Actions&LifecycleActionDelete != 0 { 348 defer func() { 349 // runtime error or the container is already deleted 350 if _, err := r.State(); err != nil { 351 return 352 } 353 r.Kill("KILL") 354 err := WaitingForStatus(r, LifecycleStatusStopped, time.Second*10, time.Second*1) 355 if err == nil { 356 r.Delete() 357 } else { 358 os.Stderr.WriteString("failed to delete the container\n") 359 os.Stderr.WriteString(err.Error()) 360 } 361 }() 362 } 363 } 364 365 if config.PostCreate != nil { 366 if err := config.PostCreate(&r); err != nil { 367 return err 368 } 369 } 370 371 if config.Actions&LifecycleActionStart != 0 { 372 err := r.Start() 373 if err != nil { 374 os.Stderr.WriteString("failed to start the container\n") 375 if e, ok := err.(*exec.ExitError); ok && len(e.Stderr) > 0 { 376 os.Stderr.Write(e.Stderr) 377 } 378 return err 379 } 380 } 381 382 if config.PreDelete != nil { 383 if err := config.PreDelete(&r); err != nil { 384 return err 385 } 386 } 387 388 if config.Actions&LifecycleActionDelete != 0 { 389 err := r.Delete() 390 if err != nil { 391 os.Stderr.WriteString("failed to delete the container\n") 392 if e, ok := err.(*exec.ExitError); ok && len(e.Stderr) > 0 { 393 os.Stderr.Write(e.Stderr) 394 } 395 return err 396 } 397 } 398 399 if config.PostDelete != nil { 400 if err := config.PostDelete(&r); err != nil { 401 return err 402 } 403 } 404 return nil 405 }